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_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]]))
class SceneNode(): class TransformSpace: Local = 1 Parent = 2 World = 3 ## Construct a scene node. # \param parent The parent of this node (if any). Only a root node should have None as a parent. # \param kwargs Keyword arguments. # Possible keywords: # - visible \type{bool} Is the SceneNode (and thus, all it's children) visible? Defaults to True # - name \type{string} Name of the SceneNode. Defaults to empty string. def __init__(self, parent=None, **kwargs): super().__init__() # Call super to make multiple inheritance work. self._children = [] # type: List[SceneNode] self._mesh_data = None # type: MeshData # Local transformation (from parent to local) self._transformation = Matrix() # type: Matrix # Convenience "components" of the transformation self._position = Vector() # type: Vector self._scale = Vector(1.0, 1.0, 1.0) # type: Vector self._shear = Vector(0.0, 0.0, 0.0) # type: Vector self._mirror = Vector(1.0, 1.0, 1.0) # type: Vector self._orientation = Quaternion() # type: Quaternion # World transformation (from root to local) self._world_transformation = Matrix() # type: Matrix # Convenience "components" of the world_transformation self._derived_position = Vector() # type: Vector self._derived_orientation = Quaternion() # type: Quaternion self._derived_scale = Vector() # type: Vector self._parent = parent # type: Optional[SceneNode] # Can this SceneNode be modified in any way? self._enabled = True # type: bool # Can this SceneNode be selected in any way? self._selectable = False # type: bool # Should the AxisAlignedBounxingBox be re-calculated? self._calculate_aabb = True # type: bool # The AxisAligned bounding box. self._aabb = None # type: Optional[AxisAlignedBox] self._bounding_box_mesh = None # type: Optional[MeshData] self._visible = kwargs.get("visible", True) # type: bool self._name = kwargs.get("name", "") # type: str self._decorators = [] # type: List[SceneNodeDecorator] ## Signals self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self) def __deepcopy__(self, memo): copy = SceneNode() copy.setTransformation(self.getLocalTransformation()) copy.setMeshData(self._mesh_data) copy.setVisible(deepcopy(self._visible, memo)) copy._selectable = deepcopy(self._selectable, memo) copy._name = deepcopy(self._name, memo) for decorator in self._decorators: copy.addDecorator(deepcopy(decorator, memo)) for child in self._children: copy.addChild(deepcopy(child, memo)) self.calculateBoundingBoxMesh() return copy ## 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) 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) ## \brief Get the parent of this node. If the node has no parent, it is the root node. # \returns SceneNode if it has a parent and None if it's the root node. def getParent(self) -> Optional["SceneNode"]: return self._parent def getMirror(self) -> Vector: return self._mirror ## Get the MeshData of the bounding box # \returns \type{MeshData} Bounding box mesh. def getBoundingBoxMesh(self) -> Optional[MeshData]: return self._bounding_box_mesh ## (re)Calculate the bounding box mesh. def calculateBoundingBoxMesh(self): aabb = self.getBoundingBox() if aabb: bounding_box_mesh = MeshBuilder() rtf = aabb.maximum lbb = aabb.minimum bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back self._bounding_box_mesh = bounding_box_mesh.build() ## Handler for the ParentChanged signal # \param node Node from which this event was triggered. def _onParentChanged(self, node: Optional["SceneNode"]): for child in self.getChildren(): child.parentChanged.emit(self) ## Signal for when a \type{SceneNodeDecorator} is added / removed. decoratorsChanged = Signal() ## Add a SceneNodeDecorator to this SceneNode. # \param \type{SceneNodeDecorator} decorator The decorator to add. def addDecorator(self, decorator: SceneNodeDecorator): if type(decorator) in [type(dec) for dec in self._decorators]: Logger.log( "w", "Unable to add the same decorator type (%s) to a SceneNode twice.", type(decorator)) return try: decorator.setNode(self) except AttributeError: Logger.logException("e", "Unable to add decorator.") return self._decorators.append(decorator) self.decoratorsChanged.emit(self) ## Get all SceneNodeDecorators that decorate this SceneNode. # \return list of all SceneNodeDecorators. def getDecorators(self) -> List[SceneNodeDecorator]: return self._decorators ## Get SceneNodeDecorators by type. # \param dec_type type of decorator to return. def getDecorator(self, dec_type) -> Optional[SceneNodeDecorator]: for decorator in self._decorators: if type(decorator) == dec_type: return decorator ## Remove all decorators def removeDecorators(self): for decorator in self._decorators: decorator.clear() self._decorators = [] self.decoratorsChanged.emit(self) ## Remove decorator by type. # \param dec_type type of the decorator to remove. def removeDecorator(self, dec_type: SceneNodeDecorator): for decorator in self._decorators: if type(decorator) == dec_type: decorator.clear() self._decorators.remove(decorator) self.decoratorsChanged.emit(self) break ## Call a decoration of this SceneNode. # SceneNodeDecorators add Decorations, which are callable functions. # \param \type{string} function The function to be called. # \param *args # \param **kwargs def callDecoration(self, function: str, *args, **kwargs): for decorator in self._decorators: if hasattr(decorator, function): try: return getattr(decorator, function)(*args, **kwargs) except Exception as e: Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e)) return None ## Does this SceneNode have a certain Decoration (as defined by a Decorator) # \param \type{string} function the function to check for. def hasDecoration(self, function: str) -> bool: for decorator in self._decorators: if hasattr(decorator, function): return True return False def getName(self) -> str: return self._name def setName(self, name: str): self._name = name ## How many nodes is this node removed from the root? # \return |tupe{int} Steps from root (0 means it -is- the root). def getDepth(self) -> int: if self._parent is None: return 0 return self._parent.getDepth() + 1 ## \brief Set the parent of this object # \param scene_node SceneNode that is the parent of this object. def setParent(self, scene_node: Optional["SceneNode"]): if self._parent: self._parent.removeChild(self) if scene_node: scene_node.addChild(self) ## Emitted whenever the parent changes. parentChanged = Signal() ## \brief Get the visibility of this node. The parents visibility overrides the visibility. # TODO: Let renderer actually use the visibility to decide whether to render or not. def isVisible(self) -> bool: if self._parent is not None and self._visible: return self._parent.isVisible() else: return self._visible ## Set the visibility of this SceneNode. def setVisible(self, visible: bool): self._visible = visible ## \brief Get the (original) mesh data from the scene node/object. # \returns MeshData def getMeshData(self) -> Optional[MeshData]: return self._mesh_data ## \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. # If this node is a group, it will recursively concatenate all child nodes/objects. # \returns MeshData def getMeshDataTransformed(self) -> Optional[MeshData]: return MeshData(vertices=self.getMeshDataTransformedVertices()) ## \brief Get the transformed vertices from this scene node/object, based on the transformation of scene nodes wrt root. # If this node is a group, it will recursively concatenate all child nodes/objects. # \return numpy.ndarray def getMeshDataTransformedVertices(self) -> numpy.ndarray: transformed_vertices = None if self.callDecoration("isGroup"): for child in self._children: tv = child.getMeshDataTransformedVertices() if transformed_vertices is None: transformed_vertices = tv else: transformed_vertices = numpy.concatenate( (transformed_vertices, tv), axis=0) else: transformed_vertices = self._mesh_data.getTransformed( self.getWorldTransformation()).getVertices() return transformed_vertices ## \brief Set the mesh of this node/object # \param mesh_data MeshData object def setMeshData(self, mesh_data: Optional[MeshData]): self._mesh_data = mesh_data self._resetAABB() self.meshDataChanged.emit(self) ## Emitted whenever the attached mesh data object changes. meshDataChanged = Signal() def _onMeshDataChanged(self): self.meshDataChanged.emit(self) ## \brief Add a child to this node and set it's parent as this node. # \params scene_node SceneNode to add. def addChild(self, scene_node: "SceneNode"): if scene_node not in self._children: scene_node.transformationChanged.connect( self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node._transformChanged() scene_node.parentChanged.emit(self) ## \brief remove a single child # \param child Scene node that needs to be removed. def removeChild(self, child: "SceneNode"): if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) self._children.remove(child) child._parent = None child._transformChanged() child.parentChanged.emit(self) self._resetAABB() self.childrenChanged.emit(self) ## \brief Removes all children and its children's children. def removeAllChildren(self): for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) ## \brief Get the list of direct children # \returns List of children def getChildren(self) -> List["SceneNode"]: return self._children def hasChildren(self) -> bool: return True if self._children else False ## \brief Get list of all children (including it's children children children etc.) # \returns list ALl children in this 'tree' def getAllChildren(self) -> List["SceneNode"]: children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children ## \brief Emitted whenever the list of children of this object or any child object changes. # \param object The object that triggered the change. childrenChanged = Signal() ## \brief Computes and returns the transformation from world to local space. # \returns 4x4 transformation matrix def getWorldTransformation(self) -> Matrix: if self._world_transformation is None: self._updateTransformation() return deepcopy(self._world_transformation) ## \brief Returns the local transformation with respect to its parent. (from parent to local) # \retuns transformation 4x4 (homogenous) matrix def getLocalTransformation(self) -> Matrix: if self._transformation is None: self._updateTransformation() return deepcopy(self._transformation) def setTransformation(self, transformation: Matrix): self._transformation = deepcopy( transformation ) # Make a copy to ensure we never change the given transformation self._transformChanged() ## Get the local orientation value. def getOrientation(self) -> Quaternion: return deepcopy(self._orientation) def getWorldOrientation(self) -> Quaternion: return deepcopy(self._derived_orientation) ## \brief Rotate the scene object (and thus its children) by given amount # # \param rotation \type{Quaternion} A quaternion indicating the amount of rotation. # \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. def rotate(self, rotation: Quaternion, transform_space: int = TransformSpace.Local): if not self._enabled: return orientation_matrix = rotation.toMatrix() if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(orientation_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local orientation of this scene node. # # \param orientation \type{Quaternion} The new orientation of this scene node. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setOrientation(self, orientation: Quaternion, transform_space: int = TransformSpace.Local): if not self._enabled or orientation == self._orientation: return new_transform_matrix = Matrix() if transform_space == SceneNode.TransformSpace.World: if self.getWorldOrientation() == orientation: return new_orientation = orientation * ( self.getWorldOrientation() * self._orientation.getInverse()).getInverse() orientation_matrix = new_orientation.toMatrix() else: # Local orientation_matrix = orientation.toMatrix() euler_angles = orientation_matrix.getEuler() new_transform_matrix.compose(scale=self._scale, angles=euler_angles, translate=self._position, shear=self._shear) self._transformation = new_transform_matrix self._transformChanged() ## Get the local scaling value. def getScale(self) -> Vector: return self._scale def getWorldScale(self) -> Vector: return self._derived_scale ## Scale the scene object (and thus its children) by given amount # # \param scale \type{Vector} A Vector with three scale values # \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. def scale(self, scale: Vector, transform_space: int = TransformSpace.Local): if not self._enabled: return scale_matrix = Matrix() scale_matrix.setByScaleVector(scale) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(scale_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local scale value. # # \param scale \type{Vector} The new scale value of the scene node. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setScale(self, scale: Vector, transform_space: int = TransformSpace.Local): if not self._enabled or scale == self._scale: return if transform_space == SceneNode.TransformSpace.Local: self.scale(scale / self._scale, SceneNode.TransformSpace.Local) return if transform_space == SceneNode.TransformSpace.World: if self.getWorldScale() == scale: return self.scale(scale / self._scale, SceneNode.TransformSpace.World) ## Get the local position. def getPosition(self) -> Vector: return self._position ## Get the position of this scene node relative to the world. def getWorldPosition(self) -> Vector: return self._derived_position ## 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. def translate(self, translation: Vector, transform_space: int = 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: world_transformation = deepcopy(self._world_transformation) self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(translation_matrix) self._transformation.multiply(world_transformation) self._transformChanged() ## Set the local position value. # # \param position The new position value of the SceneNode. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setPosition(self, position: Vector, transform_space: int = TransformSpace.Local): if not self._enabled or position == self._position: return if transform_space == SceneNode.TransformSpace.Local: self.translate(position - self._position, SceneNode.TransformSpace.Parent) if transform_space == SceneNode.TransformSpace.World: if self.getWorldPosition() == position: return self.translate(position - self._derived_position, SceneNode.TransformSpace.World) ## Signal. Emitted whenever the transformation of this object or any child object changes. # \param object The object that caused the change. transformationChanged = Signal() ## Rotate this scene node in such a way that it is looking at target. # # \param target \type{Vector} The target to look at. # \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). def lookAt(self, target: Vector, up: Vector = Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalized() up = up.normalized() s = f.cross(up).normalized() u = s.cross(f).normalized() m = Matrix([[s.x, u.x, -f.x, 0.0], [s.y, u.y, -f.y, 0.0], [s.z, u.z, -f.z, 0.0], [0.0, 0.0, 0.0, 1.0]]) self.setOrientation(Quaternion.fromMatrix(m)) ## Can be overridden by child nodes if they need to perform special rendering. # If you need to handle rendering in a special way, for example for tool handles, # you can override this method and render the node. Return True to prevent the # view from rendering any attached mesh data. # # \param renderer The renderer object to use for rendering. # # \return False if the view should render this node, True if we handle our own rendering. def render(self, renderer) -> bool: return False ## Get whether this SceneNode is enabled, that is, it can be modified in any way. def isEnabled(self) -> bool: return self._enabled ## Set whether this SceneNode is enabled. # \param enable True if this object should be enabled, False if not. # \sa isEnabled def setEnabled(self, enable: bool): self._enabled = enable ## Get whether this SceneNode can be selected. # # \note This will return false if isEnabled() returns false. def isSelectable(self) -> bool: return self._enabled and self._selectable ## Set whether this SceneNode can be selected. # # \param select True if this SceneNode should be selectable, False if not. def setSelectable(self, select: bool): self._selectable = select ## Get the bounding box of this node and its children. def getBoundingBox(self) -> Optional[AxisAlignedBox]: if not self._calculate_aabb: return None if self._aabb is None: self._calculateAABB() return self._aabb ## Set whether or not to calculate the bounding box for this node. # # \param calculate True if the bounding box should be calculated, False if not. def setCalculateBoundingBox(self, calculate: bool): self._calculate_aabb = calculate boundingBoxChanged = Signal() def getShear(self) -> Vector: return self._shear ## private: def _transformChanged(self): self._updateTransformation() self._resetAABB() self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateTransformation(self): scale, shear, euler_angles, translation, mirror = self._transformation.decompose( ) self._position = translation self._scale = scale self._shear = shear self._mirror = mirror orientation = Quaternion() euler_angle_matrix = Matrix() euler_angle_matrix.setByEuler(euler_angles.x, euler_angles.y, euler_angles.z) orientation.setByMatrix(euler_angle_matrix) self._orientation = orientation if self._parent: self._world_transformation = self._parent.getWorldTransformation( ).multiply(self._transformation, copy=True) else: self._world_transformation = self._transformation world_scale, world_shear, world_euler_angles, world_translation, world_mirror = self._world_transformation.decompose( ) self._derived_position = world_translation self._derived_scale = world_scale world_euler_angle_matrix = Matrix() world_euler_angle_matrix.setByEuler(world_euler_angles.x, world_euler_angles.y, world_euler_angles.z) self._derived_orientation.setByMatrix(world_euler_angle_matrix) def _resetAABB(self): if not self._calculate_aabb: return self._aabb = None if self.getParent(): self.getParent()._resetAABB() self.boundingBoxChanged.emit() def _calculateAABB(self): aabb = None if self._mesh_data: aabb = self._mesh_data.getExtents(self.getWorldTransformation()) else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0) position = self.getWorldPosition() aabb = AxisAlignedBox(minimum=position, maximum=position) for child in self._children: if aabb is None: aabb = child.getBoundingBox() else: aabb = aabb + child.getBoundingBox() self._aabb = aabb
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
class SceneNode(): class TransformSpace: Local = 1 Parent = 2 World = 3 ## Construct a scene node. # \param parent The parent of this node (if any). Only a root node should have None as a parent. # \param kwargs Keyword arguments. # Possible keywords: # - visible \type{bool} Is the SceneNode (and thus, all it's children) visible? Defaults to True # - name \type{string} Name of the SceneNode. Defaults to empty string. def __init__(self, parent = None, **kwargs): super().__init__() # Call super to make multiple inheritance work. self._children = [] # type: List[SceneNode] self._mesh_data = None # type: MeshData # Local transformation (from parent to local) self._transformation = Matrix() # type: Matrix # Convenience "components" of the transformation self._position = Vector() # type: Vector self._scale = Vector(1.0, 1.0, 1.0) # type: Vector self._shear = Vector(0.0, 0.0, 0.0) # type: Vector self._mirror = Vector(1.0, 1.0, 1.0) # type: Vector self._orientation = Quaternion() # type: Quaternion # World transformation (from root to local) self._world_transformation = Matrix() # type: Matrix # Convenience "components" of the world_transformation self._derived_position = Vector() # type: Vector self._derived_orientation = Quaternion() # type: Quaternion self._derived_scale = Vector() # type: Vector self._parent = parent # type: Optional[SceneNode] # Can this SceneNode be modified in any way? self._enabled = True # type: bool # Can this SceneNode be selected in any way? self._selectable = False # type: bool # Should the AxisAlignedBounxingBox be re-calculated? self._calculate_aabb = True # type: bool # The AxisAligned bounding box. self._aabb = None # type: Optional[AxisAlignedBox] self._bounding_box_mesh = None # type: Optional[MeshData] self._visible = kwargs.get("visible", True) # type: bool self._name = kwargs.get("name", "") # type: str self._decorators = [] # type: List[SceneNodeDecorator] ## Signals self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self) def __deepcopy__(self, memo): copy = SceneNode() copy.setTransformation(self.getLocalTransformation()) copy.setMeshData(self._mesh_data) copy.setVisible(deepcopy(self._visible, memo)) copy._selectable = deepcopy(self._selectable, memo) copy._name = deepcopy(self._name, memo) for decorator in self._decorators: copy.addDecorator(deepcopy(decorator, memo)) for child in self._children: copy.addChild(deepcopy(child, memo)) self.calculateBoundingBoxMesh() return copy ## 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) 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) ## \brief Get the parent of this node. If the node has no parent, it is the root node. # \returns SceneNode if it has a parent and None if it's the root node. def getParent(self) -> Optional["SceneNode"]: return self._parent def getMirror(self) -> Vector: return self._mirror ## Get the MeshData of the bounding box # \returns \type{MeshData} Bounding box mesh. def getBoundingBoxMesh(self) -> Optional[MeshData]: return self._bounding_box_mesh ## (re)Calculate the bounding box mesh. def calculateBoundingBoxMesh(self): aabb = self.getBoundingBox() if aabb: bounding_box_mesh = MeshBuilder() rtf = aabb.maximum lbb = aabb.minimum bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back self._bounding_box_mesh = bounding_box_mesh.build() ## Handler for the ParentChanged signal # \param node Node from which this event was triggered. def _onParentChanged(self, node: Optional["SceneNode"]): for child in self.getChildren(): child.parentChanged.emit(self) ## Signal for when a \type{SceneNodeDecorator} is added / removed. decoratorsChanged = Signal() ## Add a SceneNodeDecorator to this SceneNode. # \param \type{SceneNodeDecorator} decorator The decorator to add. def addDecorator(self, decorator: SceneNodeDecorator): if type(decorator) in [type(dec) for dec in self._decorators]: Logger.log("w", "Unable to add the same decorator type (%s) to a SceneNode twice.", type(decorator)) return try: decorator.setNode(self) except AttributeError: Logger.logException("e", "Unable to add decorator.") return self._decorators.append(decorator) self.decoratorsChanged.emit(self) ## Get all SceneNodeDecorators that decorate this SceneNode. # \return list of all SceneNodeDecorators. def getDecorators(self) -> List[SceneNodeDecorator]: return self._decorators ## Get SceneNodeDecorators by type. # \param dec_type type of decorator to return. def getDecorator(self, dec_type) -> Optional[SceneNodeDecorator]: for decorator in self._decorators: if type(decorator) == dec_type: return decorator ## Remove all decorators def removeDecorators(self): for decorator in self._decorators: decorator.clear() self._decorators = [] self.decoratorsChanged.emit(self) ## Remove decorator by type. # \param dec_type type of the decorator to remove. def removeDecorator(self, dec_type: SceneNodeDecorator): for decorator in self._decorators: if type(decorator) == dec_type: decorator.clear() self._decorators.remove(decorator) self.decoratorsChanged.emit(self) break ## Call a decoration of this SceneNode. # SceneNodeDecorators add Decorations, which are callable functions. # \param \type{string} function The function to be called. # \param *args # \param **kwargs def callDecoration(self, function: str, *args, **kwargs): for decorator in self._decorators: if hasattr(decorator, function): try: return getattr(decorator, function)(*args, **kwargs) except Exception as e: Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e)) return None ## Does this SceneNode have a certain Decoration (as defined by a Decorator) # \param \type{string} function the function to check for. def hasDecoration(self, function: str) -> bool: for decorator in self._decorators: if hasattr(decorator, function): return True return False def getName(self) -> str: return self._name def setName(self, name: str): self._name = name ## How many nodes is this node removed from the root? # \return |tupe{int} Steps from root (0 means it -is- the root). def getDepth(self) -> int: if self._parent is None: return 0 return self._parent.getDepth() + 1 ## \brief Set the parent of this object # \param scene_node SceneNode that is the parent of this object. def setParent(self, scene_node: Optional["SceneNode"]): if self._parent: self._parent.removeChild(self) if scene_node: scene_node.addChild(self) ## Emitted whenever the parent changes. parentChanged = Signal() ## \brief Get the visibility of this node. The parents visibility overrides the visibility. # TODO: Let renderer actually use the visibility to decide whether to render or not. def isVisible(self) -> bool: if self._parent is not None and self._visible: return self._parent.isVisible() else: return self._visible ## Set the visibility of this SceneNode. def setVisible(self, visible: bool): self._visible = visible ## \brief Get the (original) mesh data from the scene node/object. # \returns MeshData def getMeshData(self) -> Optional[MeshData]: return self._mesh_data ## \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. # \returns MeshData def getMeshDataTransformed(self) -> Optional[MeshData]: if self._mesh_data: return self._mesh_data.getTransformed(self.getWorldTransformation()) return self._mesh_data ## \brief Set the mesh of this node/object # \param mesh_data MeshData object def setMeshData(self, mesh_data: Optional[MeshData]): self._mesh_data = mesh_data self._resetAABB() self.meshDataChanged.emit(self) ## Emitted whenever the attached mesh data object changes. meshDataChanged = Signal() def _onMeshDataChanged(self): self.meshDataChanged.emit(self) ## \brief Add a child to this node and set it's parent as this node. # \params scene_node SceneNode to add. def addChild(self, scene_node: "SceneNode"): if scene_node not in self._children: scene_node.transformationChanged.connect(self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node._transformChanged() scene_node.parentChanged.emit(self) ## \brief remove a single child # \param child Scene node that needs to be removed. def removeChild(self, child: "SceneNode"): if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) self._children.remove(child) child._parent = None child._transformChanged() child.parentChanged.emit(self) self._resetAABB() self.childrenChanged.emit(self) ## \brief Removes all children and its children's children. def removeAllChildren(self): for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) ## \brief Get the list of direct children # \returns List of children def getChildren(self) -> List["SceneNode"]: return self._children def hasChildren(self) -> bool: return True if self._children else False ## \brief Get list of all children (including it's children children children etc.) # \returns list ALl children in this 'tree' def getAllChildren(self) -> List["SceneNode"]: children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children ## \brief Emitted whenever the list of children of this object or any child object changes. # \param object The object that triggered the change. childrenChanged = Signal() ## \brief Computes and returns the transformation from world to local space. # \returns 4x4 transformation matrix def getWorldTransformation(self) -> Matrix: if self._world_transformation is None: self._updateTransformation() return deepcopy(self._world_transformation) ## \brief Returns the local transformation with respect to its parent. (from parent to local) # \retuns transformation 4x4 (homogenous) matrix def getLocalTransformation(self) -> Matrix: if self._transformation is None: self._updateTransformation() return deepcopy(self._transformation) def setTransformation(self, transformation: Matrix): self._transformation = deepcopy(transformation) # Make a copy to ensure we never change the given transformation self._transformChanged() ## Get the local orientation value. def getOrientation(self) -> Quaternion: return deepcopy(self._orientation) def getWorldOrientation(self) -> Quaternion: return deepcopy(self._derived_orientation) ## \brief Rotate the scene object (and thus its children) by given amount # # \param rotation \type{Quaternion} A quaternion indicating the amount of rotation. # \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. def rotate(self, rotation: Quaternion, transform_space: int = TransformSpace.Local): if not self._enabled: return orientation_matrix = rotation.toMatrix() if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply(self._world_transformation.getInverse()) self._transformation.multiply(orientation_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local orientation of this scene node. # # \param orientation \type{Quaternion} The new orientation of this scene node. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setOrientation(self, orientation: Quaternion, transform_space: int = TransformSpace.Local): if not self._enabled or orientation == self._orientation: return new_transform_matrix = Matrix() if transform_space == SceneNode.TransformSpace.World: if self.getWorldOrientation() == orientation: return new_orientation = orientation * (self.getWorldOrientation() * self._orientation.getInverse()).getInverse() orientation_matrix = new_orientation.toMatrix() else: # Local orientation_matrix = orientation.toMatrix() euler_angles = orientation_matrix.getEuler() new_transform_matrix.compose(scale = self._scale, angles = euler_angles, translate = self._position, shear = self._shear) self._transformation = new_transform_matrix self._transformChanged() ## Get the local scaling value. def getScale(self) -> Vector: return self._scale def getWorldScale(self) -> Vector: return self._derived_scale ## Scale the scene object (and thus its children) by given amount # # \param scale \type{Vector} A Vector with three scale values # \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. def scale(self, scale: Vector, transform_space: int = TransformSpace.Local): if not self._enabled: return scale_matrix = Matrix() scale_matrix.setByScaleVector(scale) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply(self._world_transformation.getInverse()) self._transformation.multiply(scale_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local scale value. # # \param scale \type{Vector} The new scale value of the scene node. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setScale(self, scale: Vector, transform_space: int = TransformSpace.Local): if not self._enabled or scale == self._scale: return if transform_space == SceneNode.TransformSpace.Local: self.scale(scale / self._scale, SceneNode.TransformSpace.Local) return if transform_space == SceneNode.TransformSpace.World: if self.getWorldScale() == scale: return self.scale(scale / self._scale, SceneNode.TransformSpace.World) ## Get the local position. def getPosition(self) -> Vector: return self._position ## Get the position of this scene node relative to the world. def getWorldPosition(self) -> Vector: return self._derived_position ## 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. def translate(self, translation: Vector, transform_space: int = 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: world_transformation = deepcopy(self._world_transformation) self._transformation.multiply(self._world_transformation.getInverse()) self._transformation.multiply(translation_matrix) self._transformation.multiply(world_transformation) self._transformChanged() ## Set the local position value. # # \param position The new position value of the SceneNode. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setPosition(self, position: Vector, transform_space: int = TransformSpace.Local): if not self._enabled or position == self._position: return if transform_space == SceneNode.TransformSpace.Local: self.translate(position - self._position, SceneNode.TransformSpace.Parent) if transform_space == SceneNode.TransformSpace.World: if self.getWorldPosition() == position: return self.translate(position - self._derived_position, SceneNode.TransformSpace.World) ## Signal. Emitted whenever the transformation of this object or any child object changes. # \param object The object that caused the change. transformationChanged = Signal() ## Rotate this scene node in such a way that it is looking at target. # # \param target \type{Vector} The target to look at. # \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). def lookAt(self, target: Vector, up: Vector = Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalized() up = up.normalized() s = f.cross(up).normalized() u = s.cross(f).normalized() m = Matrix([ [ s.x, u.x, -f.x, 0.0], [ s.y, u.y, -f.y, 0.0], [ s.z, u.z, -f.z, 0.0], [ 0.0, 0.0, 0.0, 1.0] ]) self.setOrientation(Quaternion.fromMatrix(m)) ## Can be overridden by child nodes if they need to perform special rendering. # If you need to handle rendering in a special way, for example for tool handles, # you can override this method and render the node. Return True to prevent the # view from rendering any attached mesh data. # # \param renderer The renderer object to use for rendering. # # \return False if the view should render this node, True if we handle our own rendering. def render(self, renderer) -> bool: return False ## Get whether this SceneNode is enabled, that is, it can be modified in any way. def isEnabled(self) -> bool: return self._enabled ## Set whether this SceneNode is enabled. # \param enable True if this object should be enabled, False if not. # \sa isEnabled def setEnabled(self, enable: bool): self._enabled = enable ## Get whether this SceneNode can be selected. # # \note This will return false if isEnabled() returns false. def isSelectable(self) -> bool: return self._enabled and self._selectable ## Set whether this SceneNode can be selected. # # \param select True if this SceneNode should be selectable, False if not. def setSelectable(self, select: bool): self._selectable = select ## Get the bounding box of this node and its children. def getBoundingBox(self) -> Optional[AxisAlignedBox]: if not self._calculate_aabb: return None if self._aabb is None: self._calculateAABB() return self._aabb ## Set whether or not to calculate the bounding box for this node. # # \param calculate True if the bounding box should be calculated, False if not. def setCalculateBoundingBox(self, calculate: bool): self._calculate_aabb = calculate boundingBoxChanged = Signal() def getShear(self) -> Vector: return self._shear ## private: def _transformChanged(self): self._updateTransformation() self._resetAABB() self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateTransformation(self): scale, shear, euler_angles, translation, mirror = self._transformation.decompose() self._position = translation self._scale = scale self._shear = shear self._mirror = mirror orientation = Quaternion() euler_angle_matrix = Matrix() euler_angle_matrix.setByEuler(euler_angles.x, euler_angles.y, euler_angles.z) orientation.setByMatrix(euler_angle_matrix) self._orientation = orientation if self._parent: self._world_transformation = self._parent.getWorldTransformation().multiply(self._transformation, copy = True) else: self._world_transformation = self._transformation world_scale, world_shear, world_euler_angles, world_translation, world_mirror = self._world_transformation.decompose() self._derived_position = world_translation self._derived_scale = world_scale world_euler_angle_matrix = Matrix() world_euler_angle_matrix.setByEuler(world_euler_angles.x, world_euler_angles.y, world_euler_angles.z) self._derived_orientation.setByMatrix(world_euler_angle_matrix) def _resetAABB(self): if not self._calculate_aabb: return self._aabb = None if self.getParent(): self.getParent()._resetAABB() self.boundingBoxChanged.emit() def _calculateAABB(self): aabb = None if self._mesh_data: aabb = self._mesh_data.getExtents(self.getWorldTransformation()) else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0) position = self.getWorldPosition() aabb = AxisAlignedBox(minimum = position, maximum = position) for child in self._children: if aabb is None: aabb = child.getBoundingBox() else: aabb = aabb + child.getBoundingBox() self._aabb = aabb
class SceneNode: """A scene node object. These objects can hold a mesh and multiple children. Each node has a transformation matrix that maps it it's parents space to the local space (it's inverse maps local space to parent). SceneNodes can be "Decorated" by adding SceneNodeDecorator objects. These decorators can add functionality to scene nodes. :sa SceneNodeDecorator :todo Add unit testing """ class TransformSpace: Local = 1 #type: int Parent = 2 #type: int World = 3 #type: int def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", node_id: str = "") -> None: """Construct a scene node. :param parent: The parent of this node (if any). Only a root node should have None as a parent. :param visible: Is the SceneNode (and thus, all its children) visible? :param name: Name of the SceneNode. """ super().__init__() # Call super to make multiple inheritance work. self._children = [] # type: List[SceneNode] self._mesh_data = None # type: Optional[MeshData] # Local transformation (from parent to local) self._transformation = Matrix() # type: Matrix # Convenience "components" of the transformation self._position = Vector() # type: Vector self._scale = Vector(1.0, 1.0, 1.0) # type: Vector self._shear = Vector(0.0, 0.0, 0.0) # type: Vector self._mirror = Vector(1.0, 1.0, 1.0) # type: Vector self._orientation = Quaternion() # type: Quaternion # World transformation (from root to local) self._world_transformation = Matrix() # type: Matrix # This is used for rendering. Since we don't want to recompute it every time, we cache it in the node self._cached_normal_matrix = Matrix() # Convenience "components" of the world_transformation self._derived_position = Vector() # type: Vector self._derived_orientation = Quaternion() # type: Quaternion self._derived_scale = Vector() # type: Vector self._parent = parent # type: Optional[SceneNode] # Can this SceneNode be modified in any way? self._enabled = True # type: bool # Can this SceneNode be selected in any way? self._selectable = False # type: bool # Should the AxisAlignedBoundingBox be re-calculated? self._calculate_aabb = True # type: bool # The AxisAligned bounding box. self._aabb = None # type: Optional[AxisAlignedBox] self._bounding_box_mesh = None # type: Optional[MeshData] self._visible = visible # type: bool self._name = name # type: str self._id = node_id # type: str self._decorators = [] # type: List[SceneNodeDecorator] # Store custom settings to be compatible with Savitar SceneNode self._settings = {} # type: Dict[str, Any] ## Signals self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self) def __deepcopy__(self, memo: Dict[int, object]) -> "SceneNode": copy = self.__class__() copy.setTransformation(self.getLocalTransformation()) copy.setMeshData(self._mesh_data) copy._visible = cast(bool, deepcopy(self._visible, memo)) copy._selectable = cast(bool, deepcopy(self._selectable, memo)) copy._name = cast(str, deepcopy(self._name, memo)) for decorator in self._decorators: copy.addDecorator( cast(SceneNodeDecorator, deepcopy(decorator, memo))) for child in self._children: copy.addChild(cast(SceneNode, deepcopy(child, memo))) self.calculateBoundingBoxMesh() return copy 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 getParent(self) -> Optional["SceneNode"]: """Get the parent of this node. If the node has no parent, it is the root node. :returns: SceneNode if it has a parent and None if it's the root node. """ return self._parent def getMirror(self) -> Vector: return self._mirror def setMirror(self, vector) -> None: self._mirror = vector def getBoundingBoxMesh(self) -> Optional[MeshData]: """Get the MeshData of the bounding box :returns: :type{MeshData} Bounding box mesh. """ if self._bounding_box_mesh is None: self.calculateBoundingBoxMesh() return self._bounding_box_mesh def calculateBoundingBoxMesh(self) -> None: """(re)Calculate the bounding box mesh.""" aabb = self.getBoundingBox() if aabb: bounding_box_mesh = MeshBuilder() rtf = aabb.maximum lbb = aabb.minimum bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) # Right - Top - Front bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) # Right - Top - Back bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) # Left - Top - Front bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) # Left - Top - Back bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) # Left - Bottom - Front bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) # Left - Bottom - Back bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) # Right - Bottom - Front bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) # Right - Bottom - Back self._bounding_box_mesh = bounding_box_mesh.build() def collidesWithBbox(self, check_bbox: AxisAlignedBox) -> bool: """Return if the provided bbox collides with the bbox of this SceneNode""" bbox = self.getBoundingBox() if bbox is not None: if check_bbox.intersectsBox( bbox ) != AxisAlignedBox.IntersectionResult.FullIntersection: return True return False def _onParentChanged(self, node: Optional["SceneNode"]) -> None: """Handler for the ParentChanged signal :param node: Node from which this event was triggered. """ for child in self.getChildren(): child.parentChanged.emit(self) decoratorsChanged = Signal() """Signal for when a :type{SceneNodeDecorator} is added / removed.""" def addDecorator(self, decorator: SceneNodeDecorator) -> None: """Add a SceneNodeDecorator to this SceneNode. :param decorator: The decorator to add. """ if type(decorator) in [type(dec) for dec in self._decorators]: Logger.log( "w", "Unable to add the same decorator type (%s) to a SceneNode twice.", type(decorator)) return try: decorator.setNode(self) except AttributeError: Logger.logException("e", "Unable to add decorator.") return self._decorators.append(decorator) self.decoratorsChanged.emit(self) def getDecorators(self) -> List[SceneNodeDecorator]: """Get all SceneNodeDecorators that decorate this SceneNode. :return: list of all SceneNodeDecorators. """ return self._decorators def getDecorator(self, dec_type: type) -> Optional[SceneNodeDecorator]: """Get SceneNodeDecorators by type. :param dec_type: type of decorator to return. """ for decorator in self._decorators: if type(decorator) == dec_type: return decorator return None def removeDecorators(self): """Remove all decorators""" for decorator in self._decorators: decorator.clear() self._decorators = [] self.decoratorsChanged.emit(self) def removeDecorator(self, dec_type: type) -> None: """Remove decorator by type. :param dec_type: type of the decorator to remove. """ for decorator in self._decorators: if type(decorator) == dec_type: decorator.clear() self._decorators.remove(decorator) self.decoratorsChanged.emit(self) break def callDecoration(self, function: str, *args, **kwargs) -> Any: """Call a decoration of this SceneNode. SceneNodeDecorators add Decorations, which are callable functions. :param function: The function to be called. :param *args :param **kwargs """ for decorator in self._decorators: if hasattr(decorator, function): try: return getattr(decorator, function)(*args, **kwargs) except Exception as e: Logger.logException("e", "Exception calling decoration %s: %s", str(function), str(e)) return None def hasDecoration(self, function: str) -> bool: """Does this SceneNode have a certain Decoration (as defined by a Decorator) :param :type{string} function the function to check for. """ for decorator in self._decorators: if hasattr(decorator, function): return True return False def getName(self) -> str: return self._name def setName(self, name: str) -> None: self._name = name def getId(self) -> str: return self._id def setId(self, node_id: str) -> None: self._id = node_id def getDepth(self) -> int: """How many nodes is this node removed from the root? :return: Steps from root (0 means it -is- the root). """ if self._parent is None: return 0 return self._parent.getDepth() + 1 def setParent(self, scene_node: Optional["SceneNode"]) -> None: """:brief Set the parent of this object :param scene_node: SceneNode that is the parent of this object. """ if self._parent: self._parent.removeChild(self) if scene_node: scene_node.addChild(self) parentChanged = Signal() """Emitted whenever the parent changes.""" def isVisible(self) -> bool: """Get the visibility of this node. The parents visibility overrides the visibility. TODO: Let renderer actually use the visibility to decide whether to render or not. """ if self._parent is not None and self._visible: return self._parent.isVisible() else: return self._visible def setVisible(self, visible: bool) -> None: """Set the visibility of this SceneNode.""" self._visible = visible def getMeshData(self) -> Optional[MeshData]: """Get the (original) mesh data from the scene node/object. :returns: MeshData """ return self._mesh_data def getMeshDataTransformed(self) -> Optional[MeshData]: """Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. If this node is a group, it will recursively concatenate all child nodes/objects. :returns: MeshData """ return MeshData(vertices=self.getMeshDataTransformedVertices(), normals=self.getMeshDataTransformedNormals()) def getMeshDataTransformedVertices(self) -> numpy.ndarray: """Get the transformed vertices from this scene node/object, based on the transformation of scene nodes wrt root. If this node is a group, it will recursively concatenate all child nodes/objects. :return: numpy.ndarray """ transformed_vertices = None if self.callDecoration("isGroup"): for child in self._children: tv = child.getMeshDataTransformedVertices() if transformed_vertices is None: transformed_vertices = tv else: transformed_vertices = numpy.concatenate( (transformed_vertices, tv), axis=0) else: if self._mesh_data: transformed_vertices = self._mesh_data.getTransformed( self.getWorldTransformation(copy=False)).getVertices() return transformed_vertices def getMeshDataTransformedNormals(self) -> numpy.ndarray: """Get the transformed normals from this scene node/object, based on the transformation of scene nodes wrt root. If this node is a group, it will recursively concatenate all child nodes/objects. :return: numpy.ndarray """ transformed_normals = None if self.callDecoration("isGroup"): for child in self._children: tv = child.getMeshDataTransformedNormals() if transformed_normals is None: transformed_normals = tv else: transformed_normals = numpy.concatenate( (transformed_normals, tv), axis=0) else: if self._mesh_data: transformed_normals = self._mesh_data.getTransformed( self.getWorldTransformation(copy=False)).getNormals() return transformed_normals def setMeshData(self, mesh_data: Optional[MeshData]) -> None: """Set the mesh of this node/object :param mesh_data: MeshData object """ self._mesh_data = mesh_data self._resetAABB() self.meshDataChanged.emit(self) meshDataChanged = Signal() """Emitted whenever the attached mesh data object changes.""" def _onMeshDataChanged(self) -> None: self.meshDataChanged.emit(self) def addChild(self, scene_node: "SceneNode") -> None: """Add a child to this node and set it's parent as this node. :params scene_node SceneNode to add. """ if scene_node in self._children: return scene_node.transformationChanged.connect(self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node._transformChanged() scene_node.parentChanged.emit(self) def removeChild(self, child: "SceneNode") -> None: """remove a single child :param child: Scene node that needs to be removed. """ if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) self._children.remove(child) child._parent = None child._transformChanged() child.parentChanged.emit(self) self._resetAABB() self.childrenChanged.emit(self) def removeAllChildren(self) -> None: """Removes all children and its children's children.""" for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) def getChildren(self) -> List["SceneNode"]: """Get the list of direct children :returns: List of children """ return self._children def hasChildren(self) -> bool: return True if self._children else False def getAllChildren(self) -> List["SceneNode"]: """Get list of all children (including it's children children children etc.) :returns: list ALl children in this 'tree' """ children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children childrenChanged = Signal() """Emitted whenever the list of children of this object or any child object changes. :param object: The object that triggered the change. """ def _updateCachedNormalMatrix(self) -> None: self._cached_normal_matrix = Matrix( self.getWorldTransformation(copy=False).getData()) self._cached_normal_matrix.setRow(3, [0, 0, 0, 1]) self._cached_normal_matrix.setColumn(3, [0, 0, 0, 1]) self._cached_normal_matrix.pseudoinvert() self._cached_normal_matrix.transpose() def getCachedNormalMatrix(self) -> Matrix: if self._cached_normal_matrix is None: self._updateCachedNormalMatrix() return self._cached_normal_matrix def getWorldTransformation(self, copy=True) -> Matrix: """Computes and returns the transformation from world to local space. :returns: 4x4 transformation matrix """ if self._world_transformation is None: self._updateWorldTransformation() if copy: return self._world_transformation.copy() return self._world_transformation def getLocalTransformation(self, copy=True) -> Matrix: """Returns the local transformation with respect to its parent. (from parent to local) :returns transformation 4x4 (homogeneous) matrix """ if self._transformation is None: self._updateLocalTransformation() if copy: return self._transformation.copy() return self._transformation def setTransformation(self, transformation: Matrix): self._transformation = transformation.copy( ) # Make a copy to ensure we never change the given transformation self._transformChanged() def getOrientation(self) -> Quaternion: """Get the local orientation value.""" return deepcopy(self._orientation) def getWorldOrientation(self) -> Quaternion: return deepcopy(self._derived_orientation) def rotate(self, rotation: Quaternion, transform_space: int = TransformSpace.Local) -> None: """Rotate the scene object (and thus its children) by given amount :param rotation: :type{Quaternion} A quaternion indicating the amount of rotation. :param transform_space: The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. """ if not self._enabled: return orientation_matrix = rotation.toMatrix() if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(orientation_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() def setOrientation(self, orientation: Quaternion, transform_space: int = TransformSpace.Local) -> None: """Set the local orientation of this scene node. :param orientation: :type{Quaternion} The new orientation of this scene node. :param transform_space: The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. """ if not self._enabled or orientation == self._orientation: return if transform_space == SceneNode.TransformSpace.World: if self.getWorldOrientation() == orientation: return new_orientation = orientation * ( self.getWorldOrientation() * self._orientation.getInverse()).invert() orientation_matrix = new_orientation.toMatrix() else: # Local orientation_matrix = orientation.toMatrix() euler_angles = orientation_matrix.getEuler() new_transform_matrix = Matrix() new_transform_matrix.compose(scale=self._scale, angles=euler_angles, translate=self._position, shear=self._shear) self._transformation = new_transform_matrix self._transformChanged() def getScale(self) -> Vector: """Get the local scaling value.""" return self._scale def getWorldScale(self) -> Vector: return self._derived_scale def scale(self, scale: Vector, transform_space: int = TransformSpace.Local) -> None: """Scale the scene object (and thus its children) by given amount :param scale: :type{Vector} A Vector with three scale values :param transform_space: The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. """ if not self._enabled: return scale_matrix = Matrix() scale_matrix.setByScaleVector(scale) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(scale_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() def setScale(self, scale: Vector, transform_space: int = TransformSpace.Local) -> None: """Set the local scale value. :param scale: :type{Vector} The new scale value of the scene node. :param transform_space: The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. """ if not self._enabled or scale == self._scale: return if transform_space == SceneNode.TransformSpace.Local: self.scale(scale / self._scale, SceneNode.TransformSpace.Local) return if transform_space == SceneNode.TransformSpace.World: if self.getWorldScale() == scale: return self.scale(scale / self._scale, SceneNode.TransformSpace.World) def getPosition(self) -> Vector: """Get the local position.""" return self._position def getWorldPosition(self) -> Vector: """Get the position of this scene node relative to the world.""" return self._derived_position 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 setPosition(self, position: Vector, transform_space: int = TransformSpace.Local) -> None: """Set the local position value. :param position: The new position value of the SceneNode. :param transform_space: The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. """ if not self._enabled or position == self._position: return if transform_space == SceneNode.TransformSpace.Local: self.translate(position - self._position, SceneNode.TransformSpace.Parent) if transform_space == SceneNode.TransformSpace.World: if self.getWorldPosition() == position: return self.translate(position - self._derived_position, SceneNode.TransformSpace.World) transformationChanged = Signal() """Signal. Emitted whenever the transformation of this object or any child object changes. :param object: The object that caused the change. """ def lookAt(self, target: Vector, up: Vector = Vector.Unit_Y) -> None: """Rotate this scene node in such a way that it is looking at target. :param target: :type{Vector} The target to look at. :param up: :type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). """ if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalized() up = up.normalized() s = f.cross(up).normalized() u = s.cross(f).normalized() m = Matrix([[s.x, u.x, -f.x, 0.0], [s.y, u.y, -f.y, 0.0], [s.z, u.z, -f.z, 0.0], [0.0, 0.0, 0.0, 1.0]]) self.setOrientation(Quaternion.fromMatrix(m)) def render(self, renderer) -> bool: """Can be overridden by child nodes if they need to perform special rendering. If you need to handle rendering in a special way, for example for tool handles, you can override this method and render the node. Return True to prevent the view from rendering any attached mesh data. :param renderer: The renderer object to use for rendering. :return: False if the view should render this node, True if we handle our own rendering. """ return False def isEnabled(self) -> bool: """Get whether this SceneNode is enabled, that is, it can be modified in any way.""" if self._parent is not None and self._enabled: return self._parent.isEnabled() else: return self._enabled def setEnabled(self, enable: bool) -> None: """Set whether this SceneNode is enabled. :param enable: True if this object should be enabled, False if not. :sa isEnabled """ self._enabled = enable def isSelectable(self) -> bool: """Get whether this SceneNode can be selected. :note This will return false if isEnabled() returns false. """ return self._enabled and self._selectable def setSelectable(self, select: bool) -> None: """Set whether this SceneNode can be selected. :param select: True if this SceneNode should be selectable, False if not. """ self._selectable = select def getBoundingBox(self) -> Optional[AxisAlignedBox]: """Get the bounding box of this node and its children.""" if not self._calculate_aabb: return None if self._aabb is None: self._calculateAABB() return self._aabb def setCalculateBoundingBox(self, calculate: bool) -> None: """Set whether or not to calculate the bounding box for this node. :param calculate: True if the bounding box should be calculated, False if not. """ self._calculate_aabb = calculate boundingBoxChanged = Signal() def getShear(self) -> Vector: return self._shear def getSetting(self, key: str, default_value: str = "") -> str: return self._settings.get(key, default_value) def setSetting(self, key: str, value: str) -> None: self._settings[key] = value def invertNormals(self) -> None: for child in self._children: child.invertNormals() if self._mesh_data: self._mesh_data.invertNormals() def _transformChanged(self) -> None: self._updateTransformation() self._resetAABB() self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateLocalTransformation(self) -> None: self._position, euler_angle_matrix, self._scale, self._shear = self._transformation.decompose( ) self._orientation.setByMatrix(euler_angle_matrix) def _updateWorldTransformation(self) -> None: if self._parent: self._world_transformation = self._parent.getWorldTransformation( ).multiply(self._transformation) else: self._world_transformation = self._transformation self._derived_position, world_euler_angle_matrix, self._derived_scale, world_shear = self._world_transformation.decompose( ) self._derived_orientation.setByMatrix(world_euler_angle_matrix) def _updateTransformation(self) -> None: self._updateLocalTransformation() self._updateWorldTransformation() self._updateCachedNormalMatrix() def _resetAABB(self) -> None: if not self._calculate_aabb: return self._aabb = None self._bounding_box_mesh = None if self._parent: self._parent._resetAABB() self.boundingBoxChanged.emit() def _calculateAABB(self) -> None: if self._mesh_data: aabb = self._mesh_data.getExtents( self.getWorldTransformation(copy=False)) else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0) position = self.getWorldPosition() aabb = AxisAlignedBox(minimum=position, maximum=position) for child in self._children: if aabb is None: aabb = child.getBoundingBox() else: aabb = aabb + child.getBoundingBox() self._aabb = aabb def __str__(self) -> str: """String output for debugging.""" name = self._name if self._name != "" else hex(id(self)) return "<" + self.__class__.__qualname__ + " object: '" + name + "'>"
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 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
class SceneNode(SignalEmitter): class TransformSpace: Local = 1 Parent = 2 World = 3 def __init__(self, parent=None, **kwargs): super().__init__() # Call super to make multiple inheritence work. self._children = [] self._mesh_data = None self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._shear = Vector(0.0, 0.0, 0.0) self._orientation = Quaternion() self._transformation = Matrix() #local transformation self._world_transformation = Matrix() self._derived_position = Vector() self._derived_orientation = Quaternion() self._derived_scale = Vector() self._inherit_orientation = True self._inherit_scale = True self._parent = parent self._enabled = True self._selectable = False self._calculate_aabb = True self._aabb = None self._aabb_job = None self._visible = kwargs.get("visible", True) self._name = kwargs.get("name", "") self._decorators = [] self._bounding_box_mesh = None self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self) def __deepcopy__(self, memo): copy = SceneNode() copy.translate(self.getPosition()) copy.setOrientation(self.getOrientation()) copy.setScale(self.getScale()) copy.setMeshData(deepcopy(self._mesh_data, memo)) copy.setVisible(deepcopy(self._visible, memo)) copy._selectable = deepcopy(self._selectable, memo) for decorator in self._decorators: copy.addDecorator(deepcopy(decorator, memo)) for child in self._children: copy.addChild(deepcopy(child, memo)) self.calculateBoundingBoxMesh() return copy 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) ## \brief Get the parent of this node. If the node has no parent, it is the root node. # \returns SceneNode if it has a parent and None if it's the root node. def getParent(self): return self._parent def getBoundingBoxMesh(self): return self._bounding_box_mesh def calculateBoundingBoxMesh(self): if self._aabb: self._bounding_box_mesh = MeshData() rtf = self._aabb.maximum lbb = self._aabb.minimum self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back else: self._resetAABB() def _onParentChanged(self, node): for child in self.getChildren(): child.parentChanged.emit(self) decoratorsChanged = Signal() def addDecorator(self, decorator): decorator.setNode(self) self._decorators.append(decorator) self.decoratorsChanged.emit(self) def getDecorators(self): return self._decorators def getDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: return decorator def removeDecorators(self): self._decorators = [] self.decoratorsChanged.emit(self) def removeDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: self._decorators.remove(decorator) self.decoratorsChanged.emit(self) break def callDecoration(self, function, *args, **kwargs): for decorator in self._decorators: if hasattr(decorator, function): try: return getattr(decorator, function)(*args, **kwargs) except Exception as e: Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e)) return None def hasDecoration(self, function): for decorator in self._decorators: if hasattr(decorator, function): return True return False def getName(self): return self._name def setName(self, name): self._name = name ## How many nodes is this node removed from the root def getDepth(self): if self._parent is None: return 0 return self._parent.getDepth() + 1 ## \brief Set the parent of this object # \param scene_node SceneNode that is the parent of this object. def setParent(self, scene_node): if self._parent: self._parent.removeChild(self) #self._parent = scene_node if scene_node: scene_node.addChild(self) ## Emitted whenever the parent changes. parentChanged = Signal() ## \brief Get the visibility of this node. The parents visibility overrides the visibility. # TODO: Let renderer actually use the visibility to decide wether to render or not. def isVisible(self): if self._parent != None and self._visible: return self._parent.isVisible() else: return self._visible def setVisible(self, visible): self._visible = visible ## \brief Get the (original) mesh data from the scene node/object. # \returns MeshData def getMeshData(self): return self._mesh_data ## \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. # \returns MeshData def getMeshDataTransformed(self): #transformed_mesh = deepcopy(self._mesh_data) #transformed_mesh.transform(self.getWorldTransformation()) return self._mesh_data.getTransformed(self.getWorldTransformation()) ## \brief Set the mesh of this node/object # \param mesh_data MeshData object def setMeshData(self, mesh_data): if self._mesh_data: self._mesh_data.dataChanged.disconnect(self._onMeshDataChanged) self._mesh_data = mesh_data if self._mesh_data is not None: self._mesh_data.dataChanged.connect(self._onMeshDataChanged) self._resetAABB() self.meshDataChanged.emit(self) ## Emitted whenever the attached mesh data object changes. meshDataChanged = Signal() def _onMeshDataChanged(self): self.meshDataChanged.emit(self) ## \brief Add a child to this node and set it's parent as this node. # \params scene_node SceneNode to add. def addChild(self, scene_node): if scene_node not in self._children: scene_node.transformationChanged.connect( self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node._transformChanged() scene_node.parentChanged.emit(self) ## \brief remove a single child # \param child Scene node that needs to be removed. def removeChild(self, child): if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) self._children.remove(child) child._parent = None child._transformChanged() child.parentChanged.emit(self) self.childrenChanged.emit(self) ## \brief Removes all children and its children's children. def removeAllChildren(self): for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) ## \brief Get the list of direct children # \returns List of children def getChildren(self): return self._children def hasChildren(self): return True if self._children else False ## \brief Get list of all children (including it's children children children etc.) # \returns list ALl children in this 'tree' def getAllChildren(self): children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children ## \brief Emitted whenever the list of children of this object or any child object changes. # \param object The object that triggered the change. childrenChanged = Signal() ## \brief Computes and returns the transformation from world to local space. # \returns 4x4 transformation matrix def getWorldTransformation(self): if self._world_transformation is None: self._updateTransformation() return deepcopy(self._world_transformation) ## \brief Returns the local transformation with respect to its parent. (from parent to local) # \retuns transformation 4x4 (homogenous) matrix def getLocalTransformation(self): if self._transformation is None: self._updateTransformation() return deepcopy(self._transformation) def setTransformation(self, transformation): self._transformation = transformation self._transformChanged() ## Get the local orientation value. def getOrientation(self): return deepcopy(self._orientation) def getWorldOrientation(self): return deepcopy(self._derived_orientation) ## \brief Rotate the scene object (and thus its children) by given amount # # \param rotation \type{Quaternion} A quaternion indicating the amount of rotation. # \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. def rotate(self, rotation, transform_space=TransformSpace.Local): if not self._enabled: return orientation_matrix = rotation.toMatrix() if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(orientation_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local orientation of this scene node. # # \param orientation \type{Quaternion} The new orientation of this scene node. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setOrientation(self, orientation, transform_space=TransformSpace.Local): if not self._enabled or orientation == self._orientation: return new_transform_matrix = Matrix() if transform_space == SceneNode.TransformSpace.Local: orientation_matrix = orientation.toMatrix() if transform_space == SceneNode.TransformSpace.World: if self.getWorldOrientation() == orientation: return new_orientation = orientation * ( self.getWorldOrientation() * self._orientation.getInverse()).getInverse() orientation_matrix = new_orientation.toMatrix() euler_angles = orientation_matrix.getEuler() new_transform_matrix.compose(scale=self._scale, angles=euler_angles, translate=self._position, shear=self._shear) self._transformation = new_transform_matrix self._transformChanged() ## Get the local scaling value. def getScale(self): return deepcopy(self._scale) def getWorldScale(self): return deepcopy(self._derived_scale) ## Scale the scene object (and thus its children) by given amount # # \param scale \type{Vector} A Vector with three scale values # \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. def scale(self, scale, transform_space=TransformSpace.Local): if not self._enabled: return scale_matrix = Matrix() scale_matrix.setByScaleVector(scale) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply( self._world_transformation.getInverse()) self._transformation.multiply(scale_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local scale value. # # \param scale \type{Vector} The new scale value of the scene node. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setScale(self, scale, transform_space=TransformSpace.Local): if not self._enabled or scale == self._scale: return if transform_space == SceneNode.TransformSpace.Local: self.scale(scale / self._scale, SceneNode.TransformSpace.Local) return if transform_space == SceneNode.TransformSpace.World: if self.getWorldScale() == scale: return self.scale(scale / self._scale, SceneNode.TransformSpace.World) ## Get the local position. def getPosition(self): return deepcopy(self._position) ## Get the position of this scene node relative to the world. def getWorldPosition(self): return deepcopy(self._derived_position) ## 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. 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() ## Set the local position value. # # \param position The new position value of the SceneNode. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setPosition(self, position, transform_space=TransformSpace.Local): if not self._enabled or position == self._position: return if transform_space == SceneNode.TransformSpace.Local: self.translate(position - self._position, SceneNode.TransformSpace.Parent) if transform_space == SceneNode.TransformSpace.World: if self.getWorldPosition() == position: return self.translate(position - self._position, SceneNode.TransformSpace.World) ## Signal. Emitted whenever the transformation of this object or any child object changes. # \param object The object that caused the change. transformationChanged = Signal() ## Rotate this scene node in such a way that it is looking at target. # # \param target \type{Vector} The target to look at. # \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). def lookAt(self, target, up=Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalize() up.normalize() s = f.cross(up).normalize() u = s.cross(f).normalize() m = Matrix([[s.x, u.x, -f.x, 0.0], [s.y, u.y, -f.y, 0.0], [s.z, u.z, -f.z, 0.0], [0.0, 0.0, 0.0, 1.0]]) self.setOrientation(Quaternion.fromMatrix(m)) ## Can be overridden by child nodes if they need to perform special rendering. # If you need to handle rendering in a special way, for example for tool handles, # you can override this method and render the node. Return True to prevent the # view from rendering any attached mesh data. # # \param renderer The renderer object to use for rendering. # # \return False if the view should render this node, True if we handle our own rendering. def render(self, renderer): return False ## Get whether this SceneNode is enabled, that is, it can be modified in any way. def isEnabled(self): if self._parent != None and self._enabled: return self._parent.isEnabled() else: return self._enabled ## Set whether this SceneNode is enabled. # \param enable True if this object should be enabled, False if not. # \sa isEnabled def setEnabled(self, enable): self._enabled = enable ## Get whether this SceneNode can be selected. # # \note This will return false if isEnabled() returns false. def isSelectable(self): return self._enabled and self._selectable ## Set whether this SceneNode can be selected. # # \param select True if this SceneNode should be selectable, False if not. def setSelectable(self, select): self._selectable = select ## Get the bounding box of this node and its children. # # Note that the AABB is calculated in a separate thread. This method will return an invalid (size 0) AABB # while the calculation happens. def getBoundingBox(self): if self._aabb: return self._aabb if not self._aabb_job: self._resetAABB() return AxisAlignedBox() ## Set whether or not to calculate the bounding box for this node. # # \param calculate True if the bounding box should be calculated, False if not. def setCalculateBoundingBox(self, calculate): self._calculate_aabb = calculate boundingBoxChanged = Signal() ## private: def _transformChanged(self): self._updateTransformation() self._resetAABB() self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateTransformation(self): scale, shear, euler_angles, translation = self._transformation.decompose( ) self._position = translation self._scale = scale self._shear = shear orientation = Quaternion() euler_angle_matrix = Matrix() euler_angle_matrix.setByEuler(euler_angles.x, euler_angles.y, euler_angles.z) orientation.setByMatrix(euler_angle_matrix) self._orientation = orientation if self._parent: self._world_transformation = self._parent.getWorldTransformation( ).multiply(self._transformation, copy=True) else: self._world_transformation = self._transformation world_scale, world_shear, world_euler_angles, world_translation = self._world_transformation.decompose( ) self._derived_position = world_translation self._derived_scale = world_scale world_euler_angle_matrix = Matrix() world_euler_angle_matrix.setByEuler(world_euler_angles.x, world_euler_angles.y, world_euler_angles.z) self._derived_orientation.setByMatrix(world_euler_angle_matrix) world_scale, world_shear, world_euler_angles, world_translation = self._world_transformation.decompose( ) def _resetAABB(self): if not self._calculate_aabb: return self._aabb = None if self._aabb_job: self._aabb_job.cancel() self._aabb_job = _CalculateAABBJob(self) self._aabb_job.start()
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
class SceneNode(SignalEmitter): class TransformSpace: Local = 1 Parent = 2 World = 3 def __init__(self, parent = None, **kwargs): super().__init__() # Call super to make multiple inheritence work. self._children = [] self._mesh_data = None self._position = Vector() self._scale = Vector(1.0, 1.0, 1.0) self._shear = Vector(0.0, 0.0, 0.0) self._orientation = Quaternion() self._transformation = Matrix() #local transformation self._world_transformation = Matrix() self._derived_position = Vector() self._derived_orientation = Quaternion() self._derived_scale = Vector() self._inherit_orientation = True self._inherit_scale = True self._parent = parent self._enabled = True self._selectable = False self._calculate_aabb = True self._aabb = None self._aabb_job = None self._visible = kwargs.get("visible", True) self._name = kwargs.get("name", "") self._decorators = [] self._bounding_box_mesh = None self.boundingBoxChanged.connect(self.calculateBoundingBoxMesh) self.parentChanged.connect(self._onParentChanged) if parent: parent.addChild(self) def __deepcopy__(self, memo): copy = SceneNode() copy.translate(self.getPosition()) copy.setOrientation(self.getOrientation()) copy.setScale(self.getScale()) copy.setMeshData(deepcopy(self._mesh_data, memo)) copy.setVisible(deepcopy(self._visible, memo)) copy._selectable = deepcopy(self._selectable, memo) for decorator in self._decorators: copy.addDecorator(deepcopy(decorator, memo)) for child in self._children: copy.addChild(deepcopy(child, memo)) self.calculateBoundingBoxMesh() return copy 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) ## \brief Get the parent of this node. If the node has no parent, it is the root node. # \returns SceneNode if it has a parent and None if it's the root node. def getParent(self): return self._parent def getBoundingBoxMesh(self): return self._bounding_box_mesh def calculateBoundingBoxMesh(self): if self._aabb: self._bounding_box_mesh = MeshData() rtf = self._aabb.maximum lbb = self._aabb.minimum self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(rtf.x, rtf.y, rtf.z) #Right - Top - Front self._bounding_box_mesh.addVertex(rtf.x, rtf.y, lbb.z) #Right - Top - Back self._bounding_box_mesh.addVertex(lbb.x, rtf.y, rtf.z) #Left - Top - Front self._bounding_box_mesh.addVertex(lbb.x, rtf.y, lbb.z) #Left - Top - Back self._bounding_box_mesh.addVertex(lbb.x, lbb.y, rtf.z) #Left - Bottom - Front self._bounding_box_mesh.addVertex(lbb.x, lbb.y, lbb.z) #Left - Bottom - Back self._bounding_box_mesh.addVertex(rtf.x, lbb.y, rtf.z) #Right - Bottom - Front self._bounding_box_mesh.addVertex(rtf.x, lbb.y, lbb.z) #Right - Bottom - Back else: self._resetAABB() def _onParentChanged(self, node): for child in self.getChildren(): child.parentChanged.emit(self) decoratorsChanged = Signal() def addDecorator(self, decorator): decorator.setNode(self) self._decorators.append(decorator) self.decoratorsChanged.emit(self) def getDecorators(self): return self._decorators def getDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: return decorator def removeDecorators(self): self._decorators = [] self.decoratorsChanged.emit(self) def removeDecorator(self, dec_type): for decorator in self._decorators: if type(decorator) == dec_type: self._decorators.remove(decorator) self.decoratorsChanged.emit(self) break def callDecoration(self, function, *args, **kwargs): for decorator in self._decorators: if hasattr(decorator, function): try: return getattr(decorator, function)(*args, **kwargs) except Exception as e: Logger.log("e", "Exception calling decoration %s: %s", str(function), str(e)) return None def hasDecoration(self, function): for decorator in self._decorators: if hasattr(decorator, function): return True return False def getName(self): return self._name def setName(self, name): self._name = name ## How many nodes is this node removed from the root def getDepth(self): if self._parent is None: return 0 return self._parent.getDepth() + 1 ## \brief Set the parent of this object # \param scene_node SceneNode that is the parent of this object. def setParent(self, scene_node): if self._parent: self._parent.removeChild(self) #self._parent = scene_node if scene_node: scene_node.addChild(self) ## Emitted whenever the parent changes. parentChanged = Signal() ## \brief Get the visibility of this node. The parents visibility overrides the visibility. # TODO: Let renderer actually use the visibility to decide wether to render or not. def isVisible(self): if self._parent != None and self._visible: return self._parent.isVisible() else: return self._visible def setVisible(self, visible): self._visible = visible ## \brief Get the (original) mesh data from the scene node/object. # \returns MeshData def getMeshData(self): return self._mesh_data ## \brief Get the transformed mesh data from the scene node/object, based on the transformation of scene nodes wrt root. # \returns MeshData def getMeshDataTransformed(self): #transformed_mesh = deepcopy(self._mesh_data) #transformed_mesh.transform(self.getWorldTransformation()) return self._mesh_data.getTransformed(self.getWorldTransformation()) ## \brief Set the mesh of this node/object # \param mesh_data MeshData object def setMeshData(self, mesh_data): if self._mesh_data: self._mesh_data.dataChanged.disconnect(self._onMeshDataChanged) self._mesh_data = mesh_data if self._mesh_data is not None: self._mesh_data.dataChanged.connect(self._onMeshDataChanged) self._resetAABB() self.meshDataChanged.emit(self) ## Emitted whenever the attached mesh data object changes. meshDataChanged = Signal() def _onMeshDataChanged(self): self.meshDataChanged.emit(self) ## \brief Add a child to this node and set it's parent as this node. # \params scene_node SceneNode to add. def addChild(self, scene_node): if scene_node not in self._children: scene_node.transformationChanged.connect(self.transformationChanged) scene_node.childrenChanged.connect(self.childrenChanged) scene_node.meshDataChanged.connect(self.meshDataChanged) self._children.append(scene_node) self._resetAABB() self.childrenChanged.emit(self) if not scene_node._parent is self: scene_node._parent = self scene_node._transformChanged() scene_node.parentChanged.emit(self) ## \brief remove a single child # \param child Scene node that needs to be removed. def removeChild(self, child): if child not in self._children: return child.transformationChanged.disconnect(self.transformationChanged) child.childrenChanged.disconnect(self.childrenChanged) child.meshDataChanged.disconnect(self.meshDataChanged) self._children.remove(child) child._parent = None child._transformChanged() child.parentChanged.emit(self) self.childrenChanged.emit(self) ## \brief Removes all children and its children's children. def removeAllChildren(self): for child in self._children: child.removeAllChildren() self.removeChild(child) self.childrenChanged.emit(self) ## \brief Get the list of direct children # \returns List of children def getChildren(self): return self._children def hasChildren(self): return True if self._children else False ## \brief Get list of all children (including it's children children children etc.) # \returns list ALl children in this 'tree' def getAllChildren(self): children = [] children.extend(self._children) for child in self._children: children.extend(child.getAllChildren()) return children ## \brief Emitted whenever the list of children of this object or any child object changes. # \param object The object that triggered the change. childrenChanged = Signal() ## \brief Computes and returns the transformation from world to local space. # \returns 4x4 transformation matrix def getWorldTransformation(self): if self._world_transformation is None: self._updateTransformation() return deepcopy(self._world_transformation) ## \brief Returns the local transformation with respect to its parent. (from parent to local) # \retuns transformation 4x4 (homogenous) matrix def getLocalTransformation(self): if self._transformation is None: self._updateTransformation() return deepcopy(self._transformation) def setTransformation(self, transformation): self._transformation = transformation self._transformChanged() ## Get the local orientation value. def getOrientation(self): return deepcopy(self._orientation) def getWorldOrientation(self): return deepcopy(self._derived_orientation) ## \brief Rotate the scene object (and thus its children) by given amount # # \param rotation \type{Quaternion} A quaternion indicating the amount of rotation. # \param transform_space The space relative to which to rotate. Can be any one of the constants in SceneNode::TransformSpace. def rotate(self, rotation, transform_space = TransformSpace.Local): if not self._enabled: return orientation_matrix = rotation.toMatrix() if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(orientation_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply(self._world_transformation.getInverse()) self._transformation.multiply(orientation_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local orientation of this scene node. # # \param orientation \type{Quaternion} The new orientation of this scene node. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setOrientation(self, orientation, transform_space = TransformSpace.Local): if not self._enabled or orientation == self._orientation: return new_transform_matrix = Matrix() if transform_space == SceneNode.TransformSpace.Local: orientation_matrix = orientation.toMatrix() if transform_space == SceneNode.TransformSpace.World: if self.getWorldOrientation() == orientation: return new_orientation = orientation * (self.getWorldOrientation() * self._orientation.getInverse()).getInverse() orientation_matrix = new_orientation.toMatrix() euler_angles = orientation_matrix.getEuler() new_transform_matrix.compose(scale = self._scale, angles = euler_angles, translate = self._position, shear = self._shear) self._transformation = new_transform_matrix self._transformChanged() ## Get the local scaling value. def getScale(self): return deepcopy(self._scale) def getWorldScale(self): return deepcopy(self._derived_scale) ## Scale the scene object (and thus its children) by given amount # # \param scale \type{Vector} A Vector with three scale values # \param transform_space The space relative to which to scale. Can be any one of the constants in SceneNode::TransformSpace. def scale(self, scale, transform_space = TransformSpace.Local): if not self._enabled: return scale_matrix = Matrix() scale_matrix.setByScaleVector(scale) if transform_space == SceneNode.TransformSpace.Local: self._transformation.multiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.Parent: self._transformation.preMultiply(scale_matrix) elif transform_space == SceneNode.TransformSpace.World: self._transformation.multiply(self._world_transformation.getInverse()) self._transformation.multiply(scale_matrix) self._transformation.multiply(self._world_transformation) self._transformChanged() ## Set the local scale value. # # \param scale \type{Vector} The new scale value of the scene node. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setScale(self, scale, transform_space = TransformSpace.Local): if not self._enabled or scale == self._scale: return if transform_space == SceneNode.TransformSpace.Local: self.scale(scale / self._scale, SceneNode.TransformSpace.Local) return if transform_space == SceneNode.TransformSpace.World: if self.getWorldScale() == scale: return self.scale(scale / self._scale, SceneNode.TransformSpace.World) ## Get the local position. def getPosition(self): return deepcopy(self._position) ## Get the position of this scene node relative to the world. def getWorldPosition(self): return deepcopy(self._derived_position) ## 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. 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() ## Set the local position value. # # \param position The new position value of the SceneNode. # \param transform_space The space relative to which to rotate. Can be Local or World from SceneNode::TransformSpace. def setPosition(self, position, transform_space = TransformSpace.Local): if not self._enabled or position == self._position: return if transform_space == SceneNode.TransformSpace.Local: self.translate(position - self._position, SceneNode.TransformSpace.Parent) if transform_space == SceneNode.TransformSpace.World: if self.getWorldPosition() == position: return self.translate(position - self._position, SceneNode.TransformSpace.World) ## Signal. Emitted whenever the transformation of this object or any child object changes. # \param object The object that caused the change. transformationChanged = Signal() ## Rotate this scene node in such a way that it is looking at target. # # \param target \type{Vector} The target to look at. # \param up \type{Vector} The vector to consider up. Defaults to Vector.Unit_Y, i.e. (0, 1, 0). def lookAt(self, target, up = Vector.Unit_Y): if not self._enabled: return eye = self.getWorldPosition() f = (target - eye).normalize() up.normalize() s = f.cross(up).normalize() u = s.cross(f).normalize() m = Matrix([ [ s.x, u.x, -f.x, 0.0], [ s.y, u.y, -f.y, 0.0], [ s.z, u.z, -f.z, 0.0], [ 0.0, 0.0, 0.0, 1.0] ]) self.setOrientation(Quaternion.fromMatrix(m)) ## Can be overridden by child nodes if they need to perform special rendering. # If you need to handle rendering in a special way, for example for tool handles, # you can override this method and render the node. Return True to prevent the # view from rendering any attached mesh data. # # \param renderer The renderer object to use for rendering. # # \return False if the view should render this node, True if we handle our own rendering. def render(self, renderer): return False ## Get whether this SceneNode is enabled, that is, it can be modified in any way. def isEnabled(self): if self._parent != None and self._enabled: return self._parent.isEnabled() else: return self._enabled ## Set whether this SceneNode is enabled. # \param enable True if this object should be enabled, False if not. # \sa isEnabled def setEnabled(self, enable): self._enabled = enable ## Get whether this SceneNode can be selected. # # \note This will return false if isEnabled() returns false. def isSelectable(self): return self._enabled and self._selectable ## Set whether this SceneNode can be selected. # # \param select True if this SceneNode should be selectable, False if not. def setSelectable(self, select): self._selectable = select ## Get the bounding box of this node and its children. # # Note that the AABB is calculated in a separate thread. This method will return an invalid (size 0) AABB # while the calculation happens. def getBoundingBox(self): if self._aabb: return self._aabb if not self._aabb_job: self._resetAABB() return AxisAlignedBox() ## Set whether or not to calculate the bounding box for this node. # # \param calculate True if the bounding box should be calculated, False if not. def setCalculateBoundingBox(self, calculate): self._calculate_aabb = calculate boundingBoxChanged = Signal() ## private: def _transformChanged(self): self._updateTransformation() self._resetAABB() self.transformationChanged.emit(self) for child in self._children: child._transformChanged() def _updateTransformation(self): scale, shear, euler_angles, translation = self._transformation.decompose() self._position = translation self._scale = scale self._shear = shear orientation = Quaternion() euler_angle_matrix = Matrix() euler_angle_matrix.setByEuler(euler_angles.x, euler_angles.y, euler_angles.z) orientation.setByMatrix(euler_angle_matrix) self._orientation = orientation if self._parent: self._world_transformation = self._parent.getWorldTransformation().multiply(self._transformation, copy = True) else: self._world_transformation = self._transformation world_scale, world_shear, world_euler_angles, world_translation = self._world_transformation.decompose() self._derived_position = world_translation self._derived_scale = world_scale world_euler_angle_matrix = Matrix() world_euler_angle_matrix.setByEuler(world_euler_angles.x, world_euler_angles.y, world_euler_angles.z) self._derived_orientation.setByMatrix(world_euler_angle_matrix) world_scale, world_shear, world_euler_angles, world_translation = self._world_transformation.decompose() def _resetAABB(self): if not self._calculate_aabb: return self._aabb = None if self._aabb_job: self._aabb_job.cancel() self._aabb_job = _CalculateAABBJob(self) self._aabb_job.start()
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