def test_preMultiply(self): temp_matrix = Matrix() temp_matrix.setByTranslation(Vector(10,10,10)) temp_matrix2 = Matrix() temp_matrix2.setByScaleFactor(0.5) temp_matrix.preMultiply(temp_matrix2) numpy.testing.assert_array_almost_equal(temp_matrix.getData(), numpy.array([[0.5,0,0,5],[0,0.5,0,5],[0,0,0.5,5],[0,0,0,1]]))
def resetMeshOrigin(self) -> None: nodes_list = self._getSelectedNodes() if not nodes_list: return op = GroupedOperation() for node in nodes_list: mesh_data = node.getMeshData() if not mesh_data: continue extents = mesh_data.getExtents() center = Vector(extents.center.x, extents.center.y, extents.center.z) translation = Matrix() translation.setByTranslation(-center) transformed_mesh_data = mesh_data.getTransformed(translation).set(zero_position=Vector()) new_transformation = Matrix(node.getLocalTransformation().getData()) # Matrix.copy() is not available in Cura 3.5-4.0 new_transformation.translate(center) op.addOperation(SetMeshDataAndNameOperation(node, transformed_mesh_data, node.getName())) op.addOperation(SetTransformMatrixOperation(node, new_transformation)) op.push()
def test_setByTranslation(self): matrix = Matrix() matrix.setByTranslation(Vector(0, 1, 0)) numpy.testing.assert_array_almost_equal( matrix.getData(), numpy.array([[1, 0, 0, 0], [0, 1, 0, 1], [0, 0, 1, 0], [0, 0, 0, 1]]))
def setCenterPosition(self, center: Vector): if self._mesh_data: m = Matrix() m.setByTranslation(-center) self._mesh_data = self._mesh_data.getTransformed(m).set(center_position=center) for child in self._children: child.setCenterPosition(center)
def setCenterPosition(self, center: Vector) -> None: if self._mesh_data: m = Matrix() m.setByTranslation(-center) self._mesh_data = self._mesh_data.getTransformed(m).set(center_position=center) for child in self._children: child.setCenterPosition(center)
def read(self, file_name): result = [] # The base object of 3mf is a zipped archive. try: archive = zipfile.ZipFile(file_name, "r") self._base_name = os.path.basename(file_name) parser = Savitar.ThreeMFParser() scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read()) self._unit = scene_3mf.getUnit() for node in scene_3mf.getSceneNodes(): um_node = self._convertSavitarNodeToUMNode(node) if um_node is None: continue # compensate for original center position, if object(s) is/are not around its zero position transform_matrix = Matrix() mesh_data = um_node.getMeshData() if mesh_data is not None: extents = mesh_data.getExtents() center_vector = Vector(extents.center.x, extents.center.y, extents.center.z) transform_matrix.setByTranslation(center_vector) transform_matrix.multiply(um_node.getLocalTransformation()) um_node.setTransformation(transform_matrix) global_container_stack = Application.getInstance().getGlobalContainerStack() # Create a transformation Matrix to convert from 3mf worldspace into ours. # First step: flip the y and z axis. transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = 1 transformation_matrix._data[2, 1] = -1 transformation_matrix._data[2, 2] = 0 # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: translation_vector = Vector(x=-global_container_stack.getProperty("machine_width", "value") / 2, y=-global_container_stack.getProperty("machine_depth", "value") / 2, z=0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.multiply(translation_matrix) # Third step: 3MF also defines a unit, wheras Cura always assumes mm. scale_matrix = Matrix() scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit)) transformation_matrix.multiply(scale_matrix) # Pre multiply the transformation with the loaded transformation, so the data is handled correctly. um_node.setTransformation(um_node.getLocalTransformation().preMultiply(transformation_matrix)) result.append(um_node) except Exception: Logger.logException("e", "An exception occurred in 3mf reader.") return [] return result
def test_transposed(self): temp_matrix = Matrix() temp_matrix.setByTranslation(Vector(10, 10, 10)) temp_matrix = temp_matrix.getTransposed() numpy.testing.assert_array_almost_equal( temp_matrix.getData(), numpy.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [10, 10, 10, 1]]))
def setCenterPosition(self, center): if self._mesh_data: m = Matrix() m.setByTranslation(-center) self._mesh_data = self._mesh_data.getTransformed(m) self._mesh_data.setCenterPosition(center) for child in self._children: child.setCenterPosition(center)
class TestMatrix(unittest.TestCase): def setUp(self): self._matrix = Matrix() # Called before the first testfunction is executed pass def tearDown(self): # Called after the last testfunction was executed pass def test_setByQuaternion(self): pass def test_multiply(self): temp_matrix = Matrix() temp_matrix.setByTranslation(Vector(10,10,10)) temp_matrix2 = Matrix() temp_matrix2.setByScaleFactor(0.5) temp_matrix.multiply(temp_matrix2) numpy.testing.assert_array_almost_equal(temp_matrix.getData(), numpy.array([[0.5,0,0,10],[0,0.5,0,10],[0,0,0.5,10],[0,0,0,1]])) def test_preMultiply(self): temp_matrix = Matrix() temp_matrix.setByTranslation(Vector(10,10,10)) temp_matrix2 = Matrix() temp_matrix2.setByScaleFactor(0.5) temp_matrix.preMultiply(temp_matrix2) numpy.testing.assert_array_almost_equal(temp_matrix.getData(), numpy.array([[0.5,0,0,5],[0,0.5,0,5],[0,0,0.5,5],[0,0,0,1]])) def test_setByScaleFactor(self): self._matrix.setByScaleFactor(0.5) numpy.testing.assert_array_almost_equal(self._matrix.getData(), numpy.array([[0.5,0,0,0],[0,0.5,0,0],[0,0,0.5,0],[0,0,0,1]])) def test_setByRotation(self): pass def test_setByTranslation(self): self._matrix.setByTranslation(Vector(0,1,0)) numpy.testing.assert_array_almost_equal(self._matrix.getData(), numpy.array([[1,0,0,0],[0,1,0,1],[0,0,1,0],[0,0,0,1]])) def test_setToIdentity(self): pass def test_getData(self): pass def test_transposed(self): temp_matrix = Matrix() temp_matrix.setByTranslation(Vector(10,10,10)) temp_matrix = temp_matrix.getTransposed() numpy.testing.assert_array_almost_equal(temp_matrix.getData(), numpy.array([[1,0,0,0],[0,1,0,0],[0,0,1,0],[10,10,10,1]])) def test_dot(self): pass
def test_preMultiplyCopy(self): temp_matrix = Matrix() temp_matrix.setByTranslation(Vector(10, 10, 10)) temp_matrix2 = Matrix() temp_matrix2.setByScaleFactor(0.5) result = temp_matrix.preMultiply(temp_matrix2, copy=True) assert result != temp_matrix numpy.testing.assert_array_almost_equal( result.getData(), numpy.array([[0.5, 0, 0, 5], [0, 0.5, 0, 5], [0, 0, 0.5, 5], [0, 0, 0, 1]]))
def setUp(self): # Called before the first testfunction is executed self._scene = Scene() self._scene_object = SceneNode() self._scene_object2 = SceneNode() self._scene_object.addChild(self._scene_object2) self._scene.getRoot().addChild(self._scene_object) temp_matrix = Matrix() temp_matrix.setByTranslation(Vector(10, 10, 10)) self._scene_object2.setLocalTransformation(deepcopy(temp_matrix)) temp_matrix.setByScaleFactor(0.5) self._scene_object.setLocalTransformation(temp_matrix)
def test_transformMeshData(): transformation_matrix = Matrix() transformation_matrix.setByTranslation(Vector(30, 20, 10)) vertices = numpy.zeros((1, 3), dtype=numpy.float32) mesh_data = MeshData(vertices) transformed_mesh = mesh_data.getTransformed(transformation_matrix) assert transformed_mesh.getVertex(0)[0] == 30. assert transformed_mesh.getVertex(0)[1] == 20. assert transformed_mesh.getVertex(0)[2] == 10.
def setUp(self): # Called before the first testfunction is executed self._scene = Scene() self._scene_object = SceneNode() self._scene_object2 = SceneNode() self._scene_object.addChild(self._scene_object2) self._scene.getRoot().addChild(self._scene_object) temp_matrix = Matrix() temp_matrix.setByTranslation(Vector(10,10,10)) self._scene_object2.setLocalTransformation(deepcopy(temp_matrix)) temp_matrix.setByScaleFactor(0.5) self._scene_object.setLocalTransformation(temp_matrix)
def setCenterPosition(self, center: Vector) -> None: """Set the center position of this node. This is used to modify it's mesh data (and it's children) in such a way that they are centered. In most cases this means that we use the center of mass as center (which most objects don't use) """ if self._mesh_data: m = Matrix() m.setByTranslation(-center) self._mesh_data = self._mesh_data.getTransformed(m).set(center_position=center) for child in self._children: child.setCenterPosition(center)
def translate(self, translation, transform_space = TransformSpace.Local): if not self._enabled: return translation_matrix = Matrix() translation_matrix.setByTranslation(translation) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(translation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(translation_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply(self._world_transformation.getInverse()) self._transformation.multiply(translation_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged()
def test_getExtentsTransposed(): # Create a cube mesh at position 0,0,0 builder = MeshBuilder() builder.addCube(20, 20, 20) mesh_data = builder.build() transformation_matrix = Matrix() transformation_matrix.setByTranslation(Vector(10, 10, 10)) extents = mesh_data.getExtents(transformation_matrix) assert extents.width == 20 assert extents.height == 20 assert extents.depth == 20 assert extents.maximum == Vector(20, 20, 20) assert extents.minimum == Vector(0, 0, 0)
def read(self, file_name, storage_device, **kwargs): try: for reader in self._mesh_readers: result = reader.read(file_name, storage_device) if(result is not None): if kwargs.get("center", True): # Center the mesh extents = result.getExtents() m = Matrix() m.setByTranslation(-extents.center) result = result.getTransformed(m) result.setFileName(file_name) return result except OSError as e: Logger.log("e", str(e)) Logger.log("w", "Unable to read file %s", file_name) return None #unable to read
def translate(self, translation: Vector, transform_space: int = TransformSpace.Local) -> None: """Translate the scene object (and thus its children) by given amount. :param translation: :type{Vector} The amount to translate by. :param transform_space: The space relative to which to translate. Can be any one of the constants in SceneNode::TransformSpace. """ if not self._enabled: return translation_matrix = Matrix() translation_matrix.setByTranslation(translation) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(translation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(translation_matrix) elif transform_space == SceneNode.TransformSpace.World: world_transformation = self._world_transformation.copy() self._transformation.multiply(self._world_transformation.getInverse()) self._transformation.multiply(translation_matrix) self._transformation.multiply(world_transformation) self._transformChanged()
def render(self, renderer): if not self._shader: # We now misuse the platform shader, as it actually supports textures self._shader = OpenGL.getInstance().createShaderProgram( Resources.getPath(Resources.Shaders, "platform.shader")) # Set the opacity to 0, so that the template is in full control. self._shader.setUniformValue("u_opacity", 0) self._texture = OpenGL.getInstance().createTexture() document = QTextDocument() document.setHtml( self._getFilledTemplate(self._display_data, self._template)) texture_image = QImage(self._texture_width, self._texture_height, QImage.Format_ARGB32) texture_image.fill(Qt.transparent) painter = QPainter(texture_image) document.drawContents( painter, QRectF(0., 0., self._texture_width, self._texture_height)) painter.end() self._texture.setImage(texture_image) self._shader.setTexture(0, self._texture) node_position = self._target_node.getWorldPosition() position_matrix = Matrix() position_matrix.setByTranslation(node_position) camera_orientation = self._scene.getActiveCamera().getOrientation( ).toMatrix() renderer.queueNode(self._scene.getRoot(), shader=self._shader, transparent=True, mesh=self._billboard_mesh.getTransformed( position_matrix.multiply(camera_orientation)), sort=self._target_node.getDepth()) return True # This node does it's own rendering.
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): self._archive = None # Reset archive archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) try: model_file = zipfile.ZipInfo("3D/3dmodel.model") # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. model_file.compress_type = zipfile.ZIP_DEFLATED # Create content types file content_types_file = zipfile.ZipInfo("[Content_Types].xml") content_types_file.compress_type = zipfile.ZIP_DEFLATED content_types = ET.Element("Types", xmlns = self._namespaces["content-types"]) rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml") model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml") # Create _rels/.rels file relations_file = zipfile.ZipInfo("_rels/.rels") relations_file.compress_type = zipfile.ZIP_DEFLATED relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"]) model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") model = ET.Element("model", unit = "millimeter", xmlns = self._namespaces["3mf"]) model.set("xmlns:cura", self._namespaces["cura"]) # Add the version of Cura this was created with. Since there is no "version" or similar metadata name we need # to prefix it with the cura namespace, as specified by the 3MF specification. version_metadata = ET.SubElement(model, "metadata", name = "cura:version") version_metadata.text = Application.getInstance().getVersion() resources = ET.SubElement(model, "resources") build = ET.SubElement(model, "build") added_nodes = [] index = 0 # Ensure index always exists (even if there are no nodes to write) # Write all nodes with meshData to the file as objects inside the resource tag for index, n in enumerate(MeshWriter._meshNodes(nodes)): added_nodes.append(n) # Save the nodes that have mesh data object = ET.SubElement(resources, "object", id = str(index+1), type = "model") mesh = ET.SubElement(object, "mesh") mesh_data = n.getMeshData() vertices = ET.SubElement(mesh, "vertices") verts = mesh_data.getVertices() if verts is None: Logger.log("d", "3mf writer can't write nodes without mesh data. Skipping this node.") continue # No mesh data, nothing to do. if mesh_data.hasIndices(): for face in mesh_data.getIndices(): v1 = verts[face[0]] v2 = verts[face[1]] v3 = verts[face[2]] xml_vertex1 = ET.SubElement(vertices, "vertex", x = str(v1[0]), y = str(v1[1]), z = str(v1[2])) xml_vertex2 = ET.SubElement(vertices, "vertex", x = str(v2[0]), y = str(v2[1]), z = str(v2[2])) xml_vertex3 = ET.SubElement(vertices, "vertex", x = str(v3[0]), y = str(v3[1]), z = str(v3[2])) triangles = ET.SubElement(mesh, "triangles") for face in mesh_data.getIndices(): triangle = ET.SubElement(triangles, "triangle", v1 = str(face[0]) , v2 = str(face[1]), v3 = str(face[2])) else: triangles = ET.SubElement(mesh, "triangles") for idx, vert in enumerate(verts): xml_vertex = ET.SubElement(vertices, "vertex", x = str(vert[0]), y = str(vert[1]), z = str(vert[2])) # If we have no faces defined, assume that every three subsequent vertices form a face. if idx % 3 == 0: triangle = ET.SubElement(triangles, "triangle", v1 = str(idx), v2 = str(idx + 1), v3 = str(idx + 2)) # Handle per object settings stack = n.callDecoration("getStack") if stack is not None: changed_setting_keys = set(stack.getTop().getAllKeys()) # Ensure that we save the extruder used for this object. if stack.getProperty("machine_extruder_count", "value") > 1: changed_setting_keys.add("extruder_nr") settings_xml = ET.SubElement(object, "settings", xmlns=self._namespaces["cura"]) # Get values for all changed settings & save them. for key in changed_setting_keys: setting_xml = ET.SubElement(settings_xml, "setting", key = key) setting_xml.text = str(stack.getProperty(key, "value")) # Add one to the index as we haven't incremented the last iteration. index += 1 nodes_to_add = set() for node in added_nodes: # Check the parents of the nodes with mesh_data and ensure that they are also added. parent_node = node.getParent() while parent_node is not None: if parent_node.callDecoration("isGroup"): nodes_to_add.add(parent_node) parent_node = parent_node.getParent() else: parent_node = None # Sort all the nodes by depth (so nodes with the highest depth are done first) sorted_nodes_to_add = sorted(nodes_to_add, key=lambda node: node.getDepth(), reverse = True) # We have already saved the nodes with mesh data, but now we also want to save nodes required for the scene for node in sorted_nodes_to_add: object = ET.SubElement(resources, "object", id=str(index + 1), type="model") components = ET.SubElement(object, "components") for child in node.getChildren(): if child in added_nodes: component = ET.SubElement(components, "component", objectid = str(added_nodes.index(child) + 1), transform = self._convertMatrixToString(child.getLocalTransformation())) index += 1 added_nodes.append(node) # Create a transformation Matrix to convert from our worldspace into 3MF. # First step: flip the y and z axis. transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = -1 transformation_matrix._data[2, 1] = 1 transformation_matrix._data[2, 2] = 0 global_container_stack = Application.getInstance().getGlobalContainerStack() # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2, y=global_container_stack.getProperty("machine_depth", "value") / 2, z=0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.preMultiply(translation_matrix) # Find out what the final build items are and add them. for node in added_nodes: if node.getParent().callDecoration("isGroup") is None: node_matrix = node.getLocalTransformation() ET.SubElement(build, "item", objectid = str(added_nodes.index(node) + 1), transform = self._convertMatrixToString(node_matrix.preMultiply(transformation_matrix))) archive.writestr(model_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(model)) archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types)) archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element)) except Exception as e: Logger.logException("e", "Error writing zip file") return False finally: if not self._store_archive: archive.close() else: self._archive = archive return True
def read(self, file_name): result = [] # The base object of 3mf is a zipped archive. archive = zipfile.ZipFile(file_name, "r") self._base_name = os.path.basename(file_name) try: self._root = ET.parse(archive.open("3D/3dmodel.model")) self._unit = self._root.getroot().get("unit") build_items = self._root.findall("./3mf:build/3mf:item", self._namespaces) 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) if "type" in object.attrib: if object.attrib["type"] == "support" or object.attrib["type"] == "other": # Ignore support objects, as cura does not support these. # We can't guarantee that they wont be made solid. # We also ignore "other", as I have no idea what to do with them. Logger.log("w", "3MF file contained an object of type %s which is not supported by Cura", object.attrib["type"]) continue elif object.attrib["type"] == "solidsupport" or object.attrib["type"] == "model": pass # Load these as normal else: # We should technically fail at this point because it's an invalid 3MF, but try to continue anyway. Logger.log("e", "3MF file contained an object of type %s which is not supported by the 3mf spec", object.attrib["type"]) continue build_item_node = self._createNodeFromObject(object, self._base_name + "_" + str(id)) # compensate for original center position, if object(s) is/are not around its zero position transform_matrix = Matrix() mesh_data = build_item_node.getMeshData() if mesh_data is not None: extents = mesh_data.getExtents() center_vector = Vector(extents.center.x, extents.center.y, extents.center.z) transform_matrix.setByTranslation(center_vector) # offset with transform from 3mf transform = build_item.get("transform") if transform is not None: transform_matrix.multiply(self._createMatrixFromTransformationString(transform)) build_item_node.setTransformation(transform_matrix) global_container_stack = UM.Application.getInstance().getGlobalContainerStack() # Create a transformation Matrix to convert from 3mf worldspace into ours. # First step: flip the y and z axis. transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = 1 transformation_matrix._data[2, 1] = -1 transformation_matrix._data[2, 2] = 0 # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: translation_vector = Vector(x = -global_container_stack.getProperty("machine_width", "value") / 2, y = -global_container_stack.getProperty("machine_depth", "value") / 2, z = 0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.multiply(translation_matrix) # Third step: 3MF also defines a unit, wheras Cura always assumes mm. scale_matrix = Matrix() scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit)) transformation_matrix.multiply(scale_matrix) # Pre multiply the transformation with the loaded transformation, so the data is handled correctly. build_item_node.setTransformation(build_item_node.getLocalTransformation().preMultiply(transformation_matrix)) result.append(build_item_node) except Exception as e: Logger.log("e", "An exception occurred in 3mf reader: %s", e) return result
def test_transposed(self): temp_matrix = Matrix() temp_matrix.setByTranslation(Vector(10,10,10)) temp_matrix = temp_matrix.getTransposed() numpy.testing.assert_array_almost_equal(temp_matrix.getData(), numpy.array([[1,0,0,0],[0,1,0,0],[0,0,1,0],[10,10,10,1]]))
def test_setByTranslation(self): matrix = Matrix() matrix.setByTranslation(Vector(0,1,0)) numpy.testing.assert_array_almost_equal(matrix.getData(), numpy.array([[1,0,0,0],[0,1,0,1],[0,0,1,0],[0,0,0,1]]))
def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]: result = [] self._object_count = 0 # Used to name objects as there is no node name yet. # The base object of 3mf is a zipped archive. try: archive = zipfile.ZipFile(file_name, "r") self._base_name = os.path.basename(file_name) parser = Savitar.ThreeMFParser() scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read()) self._unit = scene_3mf.getUnit() for node in scene_3mf.getSceneNodes(): um_node = self._convertSavitarNodeToUMNode(node) if um_node is None: continue # compensate for original center position, if object(s) is/are not around its zero position transform_matrix = Matrix() mesh_data = um_node.getMeshData() if mesh_data is not None: extents = mesh_data.getExtents() if extents is not None: center_vector = Vector(extents.center.x, extents.center.y, extents.center.z) transform_matrix.setByTranslation(center_vector) transform_matrix.multiply(um_node.getLocalTransformation()) um_node.setTransformation(transform_matrix) global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() # Create a transformation Matrix to convert from 3mf worldspace into ours. # First step: flip the y and z axis. transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = 1 transformation_matrix._data[2, 1] = -1 transformation_matrix._data[2, 2] = 0 # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: translation_vector = Vector(x = -global_container_stack.getProperty("machine_width", "value") / 2, y = -global_container_stack.getProperty("machine_depth", "value") / 2, z = 0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.multiply(translation_matrix) # Third step: 3MF also defines a unit, whereas Cura always assumes mm. scale_matrix = Matrix() scale_matrix.setByScaleVector(self._getScaleFromUnit(self._unit)) transformation_matrix.multiply(scale_matrix) # Pre multiply the transformation with the loaded transformation, so the data is handled correctly. um_node.setTransformation(um_node.getLocalTransformation().preMultiply(transformation_matrix)) # Check if the model is positioned below the build plate and honor that when loading project files. node_meshdata = um_node.getMeshData() if node_meshdata is not None: aabb = node_meshdata.getExtents(um_node.getWorldTransformation()) if aabb is not None: minimum_z_value = aabb.minimum.y # y is z in transformation coordinates if minimum_z_value < 0: um_node.addDecorator(ZOffsetDecorator()) um_node.callDecoration("setZOffset", minimum_z_value) result.append(um_node) except Exception: Logger.logException("e", "An exception occurred in 3mf reader.") return [] return result
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): self._archive = None # Reset archive archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) try: model_file = zipfile.ZipInfo("3D/3dmodel.model") # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. model_file.compress_type = zipfile.ZIP_DEFLATED # Create content types file content_types_file = zipfile.ZipInfo("[Content_Types].xml") content_types_file.compress_type = zipfile.ZIP_DEFLATED content_types = ET.Element("Types", xmlns = self._namespaces["content-types"]) rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml") model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml") # Create _rels/.rels file relations_file = zipfile.ZipInfo("_rels/.rels") relations_file.compress_type = zipfile.ZIP_DEFLATED relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"]) model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") savitar_scene = Savitar.Scene() transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = -1 transformation_matrix._data[2, 1] = 1 transformation_matrix._data[2, 2] = 0 global_container_stack = Application.getInstance().getGlobalContainerStack() # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2, y=global_container_stack.getProperty("machine_depth", "value") / 2, z=0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.preMultiply(translation_matrix) root_node = UM.Application.Application.getInstance().getController().getScene().getRoot() for node in nodes: if node == root_node: for root_child in node.getChildren(): savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix) if savitar_node: savitar_scene.addSceneNode(savitar_node) else: savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix) if savitar_node: savitar_scene.addSceneNode(savitar_node) parser = Savitar.ThreeMFParser() scene_string = parser.sceneToString(savitar_scene) archive.writestr(model_file, scene_string) archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types)) archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element)) except Exception as e: Logger.logException("e", "Error writing zip file") return False finally: if not self._store_archive: archive.close() else: self._archive = archive return True
def write(self, stream, nodes, mode=MeshWriter.OutputMode.BinaryMode): try: MeshWriter._meshNodes(nodes).__next__() except StopIteration: return False #Don't write anything if there is no mesh data. archive = zipfile.ZipFile(stream, "w", compression=zipfile.ZIP_DEFLATED) try: model_file = zipfile.ZipInfo("3D/3dmodel.model") # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. model_file.compress_type = zipfile.ZIP_DEFLATED # Create content types file content_types_file = zipfile.ZipInfo("[Content_Types].xml") content_types_file.compress_type = zipfile.ZIP_DEFLATED content_types = ET.Element("Types", xmlns=self._namespaces["content-types"]) rels_type = ET.SubElement( content_types, "Default", Extension="rels", ContentType= "application/vnd.openxmlformats-package.relationships+xml") model_type = ET.SubElement( content_types, "Default", Extension="model", ContentType= "application/vnd.ms-package.3dmanufacturing-3dmodel+xml") # Create _rels/.rels file relations_file = zipfile.ZipInfo("_rels/.rels") relations_file.compress_type = zipfile.ZIP_DEFLATED relations_element = ET.Element( "Relationships", xmlns=self._namespaces["relationships"]) model_relation_element = ET.SubElement( relations_element, "Relationship", Target="/3D/3dmodel.model", Id="rel0", Type= "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") model = ET.Element("model", unit="millimeter", xmlns=self._namespaces["3mf"]) resources = ET.SubElement(model, "resources") build = ET.SubElement(model, "build") for index, n in enumerate(MeshWriter._meshNodes(nodes)): object = ET.SubElement(resources, "object", id=str(index + 1), type="model") mesh = ET.SubElement(object, "mesh") mesh_data = n.getMeshData() vertices = ET.SubElement(mesh, "vertices") verts = mesh_data.getVertices() if verts is None: Logger.log( "d", "3mf writer can't write nodes without mesh data. Skipping this node." ) continue # No mesh data, nothing to do. if mesh_data.hasIndices(): for face in mesh_data.getIndices(): v1 = verts[face[0]] v2 = verts[face[1]] v3 = verts[face[2]] xml_vertex1 = ET.SubElement(vertices, "vertex", x=str(v1[0]), y=str(v1[2]), z=str(v1[1])) xml_vertex2 = ET.SubElement(vertices, "vertex", x=str(v2[0]), y=str(v2[2]), z=str(v2[1])) xml_vertex3 = ET.SubElement(vertices, "vertex", x=str(v3[0]), y=str(v3[2]), z=str(v3[1])) triangles = ET.SubElement(mesh, "triangles") for face in mesh_data.getIndices(): triangle = ET.SubElement(triangles, "triangle", v1=str(face[0]), v2=str(face[1]), v3=str(face[2])) else: triangles = ET.SubElement(mesh, "triangles") for idx, vert in enumerate(verts): xml_vertex = ET.SubElement(vertices, "vertex", x=str(vert[0]), y=str(vert[2]), z=str(vert[1])) # If we have no faces defined, assume that every three subsequent vertices form a face. if idx % 3 == 0: triangle = ET.SubElement(triangles, "triangle", v1=str(idx), v2=str(idx + 1), v3=str(idx + 2)) world_transformation = n.getWorldTransformation() # 3MF sees lower left corner of buildplate as zero, so we need to translate a bit first 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) else: translation = Vector(0, 0, 0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation) world_transformation.multiply(translation_matrix) # We use a different coordinate frame, so we need to flip the transformation flip_matrix = Matrix() flip_matrix._data[1, 1] = 0 flip_matrix._data[1, 2] = 1 flip_matrix._data[2, 1] = 1 flip_matrix._data[2, 2] = 0 world_transformation.multiply(flip_matrix) transformation_string = self._convertMatrixToString( world_transformation) if transformation_string != self._convertMatrixToString( Matrix()): item = ET.SubElement(build, "item", objectid=str(index + 1), transform=transformation_string) else: item = ET.SubElement( build, "item", objectid=str(index + 1)) #, transform = transformation_string) archive.writestr( model_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(model)) archive.writestr( content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types)) archive.writestr( relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element)) except Exception as e: Logger.logException("e", "Error writing zip file") return False finally: archive.close() return True
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): self._archive = None # Reset archive archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) try: model_file = zipfile.ZipInfo("3D/3dmodel.model") # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. model_file.compress_type = zipfile.ZIP_DEFLATED # Create content types file content_types_file = zipfile.ZipInfo("[Content_Types].xml") content_types_file.compress_type = zipfile.ZIP_DEFLATED content_types = ET.Element("Types", xmlns = self._namespaces["content-types"]) rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml") model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml") # Create _rels/.rels file relations_file = zipfile.ZipInfo("_rels/.rels") relations_file.compress_type = zipfile.ZIP_DEFLATED relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"]) model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") model = ET.Element("model", unit = "millimeter", xmlns = self._namespaces["3mf"]) # Add the version of Cura this was created with. As "CuraVersion" is not a recognised metadata name # by 3mf itself, we place it in our own namespace. version_metadata = ET.SubElement(model, "metadata", xmlns = self._namespaces["cura"], name = "CuraVersion") version_metadata.text = Application.getInstance().getVersion() resources = ET.SubElement(model, "resources") build = ET.SubElement(model, "build") added_nodes = [] index = 0 # Ensure index always exists (even if there are no nodes to write) # Write all nodes with meshData to the file as objects inside the resource tag for index, n in enumerate(MeshWriter._meshNodes(nodes)): added_nodes.append(n) # Save the nodes that have mesh data object = ET.SubElement(resources, "object", id = str(index+1), type = "model") mesh = ET.SubElement(object, "mesh") mesh_data = n.getMeshData() vertices = ET.SubElement(mesh, "vertices") verts = mesh_data.getVertices() if verts is None: Logger.log("d", "3mf writer can't write nodes without mesh data. Skipping this node.") continue # No mesh data, nothing to do. if mesh_data.hasIndices(): for face in mesh_data.getIndices(): v1 = verts[face[0]] v2 = verts[face[1]] v3 = verts[face[2]] xml_vertex1 = ET.SubElement(vertices, "vertex", x = str(v1[0]), y = str(v1[1]), z = str(v1[2])) xml_vertex2 = ET.SubElement(vertices, "vertex", x = str(v2[0]), y = str(v2[1]), z = str(v2[2])) xml_vertex3 = ET.SubElement(vertices, "vertex", x = str(v3[0]), y = str(v3[1]), z = str(v3[2])) triangles = ET.SubElement(mesh, "triangles") for face in mesh_data.getIndices(): triangle = ET.SubElement(triangles, "triangle", v1 = str(face[0]) , v2 = str(face[1]), v3 = str(face[2])) else: triangles = ET.SubElement(mesh, "triangles") for idx, vert in enumerate(verts): xml_vertex = ET.SubElement(vertices, "vertex", x = str(vert[0]), y = str(vert[1]), z = str(vert[2])) # If we have no faces defined, assume that every three subsequent vertices form a face. if idx % 3 == 0: triangle = ET.SubElement(triangles, "triangle", v1 = str(idx), v2 = str(idx + 1), v3 = str(idx + 2)) # Handle per object settings stack = n.callDecoration("getStack") if stack is not None: changed_setting_keys = set(stack.getTop().getAllKeys()) # Ensure that we save the extruder used for this object. if stack.getProperty("machine_extruder_count", "value") > 1: changed_setting_keys.add("extruder_nr") settings_xml = ET.SubElement(object, "settings", xmlns=self._namespaces["cura"]) # Get values for all changed settings & save them. for key in changed_setting_keys: setting_xml = ET.SubElement(settings_xml, "setting", key = key) setting_xml.text = str(stack.getProperty(key, "value")) # Add one to the index as we haven't incremented the last iteration. index += 1 nodes_to_add = set() for node in added_nodes: # Check the parents of the nodes with mesh_data and ensure that they are also added. parent_node = node.getParent() while parent_node is not None: if parent_node.callDecoration("isGroup"): nodes_to_add.add(parent_node) parent_node = parent_node.getParent() else: parent_node = None # Sort all the nodes by depth (so nodes with the highest depth are done first) sorted_nodes_to_add = sorted(nodes_to_add, key=lambda node: node.getDepth(), reverse = True) # We have already saved the nodes with mesh data, but now we also want to save nodes required for the scene for node in sorted_nodes_to_add: object = ET.SubElement(resources, "object", id=str(index + 1), type="model") components = ET.SubElement(object, "components") for child in node.getChildren(): if child in added_nodes: component = ET.SubElement(components, "component", objectid = str(added_nodes.index(child) + 1), transform = self._convertMatrixToString(child.getLocalTransformation())) index += 1 added_nodes.append(node) # Create a transformation Matrix to convert from our worldspace into 3MF. # First step: flip the y and z axis. transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = -1 transformation_matrix._data[2, 1] = 1 transformation_matrix._data[2, 2] = 0 global_container_stack = UM.Application.getInstance().getGlobalContainerStack() # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2, y=global_container_stack.getProperty("machine_depth", "value") / 2, z=0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.preMultiply(translation_matrix) # Find out what the final build items are and add them. for node in added_nodes: if node.getParent().callDecoration("isGroup") is None: node_matrix = node.getLocalTransformation() ET.SubElement(build, "item", objectid = str(added_nodes.index(node) + 1), transform = self._convertMatrixToString(node_matrix.preMultiply(transformation_matrix))) archive.writestr(model_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(model)) archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types)) archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element)) except Exception as e: Logger.logException("e", "Error writing zip file") return False finally: if not self._store_archive: archive.close() else: self._archive = archive return True
def read(self, file_name): result = [] # The base object of 3mf is a zipped archive. archive = zipfile.ZipFile(file_name, "r") self._base_name = os.path.basename(file_name) try: self._root = ET.parse(archive.open("3D/3dmodel.model")) self._unit = self._root.getroot().get("unit") build_items = self._root.findall("./3mf:build/3mf:item", self._namespaces) 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) if "type" in object.attrib: if object.attrib["type"] == "support" or object.attrib[ "type"] == "other": # Ignore support objects, as cura does not support these. # We can't guarantee that they wont be made solid. # We also ignore "other", as I have no idea what to do with them. Logger.log( "w", "3MF file contained an object of type %s which is not supported by Cura", object.attrib["type"]) continue elif object.attrib[ "type"] == "solidsupport" or object.attrib[ "type"] == "model": pass # Load these as normal else: # We should technically fail at this point because it's an invalid 3MF, but try to continue anyway. Logger.log( "e", "3MF file contained an object of type %s which is not supported by the 3mf spec", object.attrib["type"]) continue build_item_node = self._createNodeFromObject( object, self._base_name + "_" + str(id)) # compensate for original center position, if object(s) is/are not around its zero position transform_matrix = Matrix() mesh_data = build_item_node.getMeshData() if mesh_data is not None: extents = mesh_data.getExtents() center_vector = Vector(extents.center.x, extents.center.y, extents.center.z) transform_matrix.setByTranslation(center_vector) # offset with transform from 3mf transform = build_item.get("transform") if transform is not None: transform_matrix.multiply( self._createMatrixFromTransformationString(transform)) build_item_node.setTransformation(transform_matrix) global_container_stack = UM.Application.getInstance( ).getGlobalContainerStack() # Create a transformation Matrix to convert from 3mf worldspace into ours. # First step: flip the y and z axis. transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = 1 transformation_matrix._data[2, 1] = -1 transformation_matrix._data[2, 2] = 0 # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: translation_vector = Vector( x=-global_container_stack.getProperty( "machine_width", "value") / 2, y=-global_container_stack.getProperty( "machine_depth", "value") / 2, z=0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.multiply(translation_matrix) # Third step: 3MF also defines a unit, wheras Cura always assumes mm. scale_matrix = Matrix() scale_matrix.setByScaleVector( self._getScaleFromUnit(self._unit)) transformation_matrix.multiply(scale_matrix) # Pre multiply the transformation with the loaded transformation, so the data is handled correctly. build_item_node.setTransformation( build_item_node.getLocalTransformation().preMultiply( transformation_matrix)) result.append(build_item_node) except Exception as e: Logger.log("e", "An exception occurred in 3mf reader: %s", e) return result
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): self._archive = None # Reset archive archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) try: model_file = zipfile.ZipInfo("3D/3dmodel.model") # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. model_file.compress_type = zipfile.ZIP_DEFLATED # Create content types file content_types_file = zipfile.ZipInfo("[Content_Types].xml") content_types_file.compress_type = zipfile.ZIP_DEFLATED content_types = ET.Element("Types", xmlns = self._namespaces["content-types"]) rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml") model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml") # Create _rels/.rels file relations_file = zipfile.ZipInfo("_rels/.rels") relations_file.compress_type = zipfile.ZIP_DEFLATED relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"]) model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") savitar_scene = Savitar.Scene() transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = -1 transformation_matrix._data[2, 1] = 1 transformation_matrix._data[2, 2] = 0 global_container_stack = Application.getInstance().getGlobalContainerStack() # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2, y=global_container_stack.getProperty("machine_depth", "value") / 2, z=0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.preMultiply(translation_matrix) root_node = UM.Application.Application.getInstance().getController().getScene().getRoot() for node in nodes: if node == root_node: for root_child in node.getChildren(): savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix) if savitar_node: savitar_scene.addSceneNode(savitar_node) else: savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix) if savitar_node: savitar_scene.addSceneNode(savitar_node) parser = Savitar.ThreeMFParser() scene_string = parser.sceneToString(savitar_scene) archive.writestr(model_file, scene_string) archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types)) archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element)) except Exception as e: Logger.logException("e", "Error writing zip file") self.setInformation(catalog.i18nc("@error:zip", "Error writing 3mf file.")) return False finally: if not self._store_archive: archive.close() else: self._archive = archive return True
def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]: self._empty_project = False result = [] # The base object of 3mf is a zipped archive. try: archive = zipfile.ZipFile(file_name, "r") self._base_name = os.path.basename(file_name) parser = Savitar.ThreeMFParser() scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read()) self._unit = scene_3mf.getUnit() for key, value in scene_3mf.getMetadata().items(): CuraApplication.getInstance().getController().getScene( ).setMetaDataEntry(key, value) for node in scene_3mf.getSceneNodes(): um_node = self._convertSavitarNodeToUMNode(node, file_name) if um_node is None: continue # compensate for original center position, if object(s) is/are not around its zero position transform_matrix = Matrix() mesh_data = um_node.getMeshData() if mesh_data is not None: extents = mesh_data.getExtents() if extents is not None: center_vector = Vector(extents.center.x, extents.center.y, extents.center.z) transform_matrix.setByTranslation(center_vector) transform_matrix.multiply(um_node.getLocalTransformation()) um_node.setTransformation(transform_matrix) global_container_stack = CuraApplication.getInstance( ).getGlobalContainerStack() # Create a transformation Matrix to convert from 3mf worldspace into ours. # First step: flip the y and z axis. transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = 1 transformation_matrix._data[2, 1] = -1 transformation_matrix._data[2, 2] = 0 # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: translation_vector = Vector( x=-global_container_stack.getProperty( "machine_width", "value") / 2, y=-global_container_stack.getProperty( "machine_depth", "value") / 2, z=0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.multiply(translation_matrix) # Third step: 3MF also defines a unit, whereas Cura always assumes mm. scale_matrix = Matrix() scale_matrix.setByScaleVector( self._getScaleFromUnit(self._unit)) transformation_matrix.multiply(scale_matrix) # Pre multiply the transformation with the loaded transformation, so the data is handled correctly. um_node.setTransformation( um_node.getLocalTransformation().preMultiply( transformation_matrix)) # Check if the model is positioned below the build plate and honor that when loading project files. node_meshdata = um_node.getMeshData() if node_meshdata is not None: aabb = node_meshdata.getExtents( um_node.getWorldTransformation()) if aabb is not None: minimum_z_value = aabb.minimum.y # y is z in transformation coordinates if minimum_z_value < 0: um_node.addDecorator(ZOffsetDecorator()) um_node.callDecoration("setZOffset", minimum_z_value) result.append(um_node) if len(result) == 0: self._empty_project = True except Exception: Logger.logException("e", "An exception occurred in 3mf reader.") return [] return result
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): self._archive = None # Reset archive archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED) try: model_file = zipfile.ZipInfo("3D/3dmodel.model") # Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo. model_file.compress_type = zipfile.ZIP_DEFLATED # Create content types file content_types_file = zipfile.ZipInfo("[Content_Types].xml") content_types_file.compress_type = zipfile.ZIP_DEFLATED content_types = ET.Element("Types", xmlns = self._namespaces["content-types"]) rels_type = ET.SubElement(content_types, "Default", Extension = "rels", ContentType = "application/vnd.openxmlformats-package.relationships+xml") model_type = ET.SubElement(content_types, "Default", Extension = "model", ContentType = "application/vnd.ms-package.3dmanufacturing-3dmodel+xml") # Create _rels/.rels file relations_file = zipfile.ZipInfo("_rels/.rels") relations_file.compress_type = zipfile.ZIP_DEFLATED relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"]) model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel") # Attempt to add a thumbnail snapshot = self._createSnapshot() if snapshot: thumbnail_buffer = QBuffer() thumbnail_buffer.open(QBuffer.ReadWrite) snapshot.save(thumbnail_buffer, "PNG") thumbnail_file = zipfile.ZipInfo("Metadata/thumbnail.png") # Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get archive.writestr(thumbnail_file, thumbnail_buffer.data()) # Add PNG to content types file thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png") # Add thumbnail relation to _rels/.rels file thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/Metadata/thumbnail.png", Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail") savitar_scene = Savitar.Scene() metadata_to_store = CuraApplication.getInstance().getController().getScene().getMetaData() for key, value in metadata_to_store.items(): savitar_scene.setMetaDataEntry(key, value) current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") if "Application" not in metadata_to_store: # This might sound a bit strange, but this field should store the original application that created # the 3mf. So if it was already set, leave it to whatever it was. savitar_scene.setMetaDataEntry("Application", CuraApplication.getInstance().getApplicationDisplayName()) if "CreationDate" not in metadata_to_store: savitar_scene.setMetaDataEntry("CreationDate", current_time_string) savitar_scene.setMetaDataEntry("ModificationDate", current_time_string) transformation_matrix = Matrix() transformation_matrix._data[1, 1] = 0 transformation_matrix._data[1, 2] = -1 transformation_matrix._data[2, 1] = 1 transformation_matrix._data[2, 2] = 0 global_container_stack = Application.getInstance().getGlobalContainerStack() # Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the # build volume. if global_container_stack: translation_vector = Vector(x=global_container_stack.getProperty("machine_width", "value") / 2, y=global_container_stack.getProperty("machine_depth", "value") / 2, z=0) translation_matrix = Matrix() translation_matrix.setByTranslation(translation_vector) transformation_matrix.preMultiply(translation_matrix) root_node = UM.Application.Application.getInstance().getController().getScene().getRoot() for node in nodes: if node == root_node: for root_child in node.getChildren(): savitar_node = self._convertUMNodeToSavitarNode(root_child, transformation_matrix) if savitar_node: savitar_scene.addSceneNode(savitar_node) else: savitar_node = self._convertUMNodeToSavitarNode(node, transformation_matrix) if savitar_node: savitar_scene.addSceneNode(savitar_node) parser = Savitar.ThreeMFParser() scene_string = parser.sceneToString(savitar_scene) archive.writestr(model_file, scene_string) archive.writestr(content_types_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(content_types)) archive.writestr(relations_file, b'<?xml version="1.0" encoding="UTF-8"?> \n' + ET.tostring(relations_element)) except Exception as e: Logger.logException("e", "Error writing zip file") self.setInformation(catalog.i18nc("@error:zip", "Error writing 3mf file.")) return False finally: if not self._store_archive: archive.close() else: self._archive = archive return True