def deleteSelection(self) -> None: if not cura.CuraApplication.CuraApplication.getInstance( ).getController().getToolsEnabled(): return removed_group_nodes = [] #type: List[SceneNode] op = GroupedOperation() nodes = Selection.getAllSelectedObjects() for node in nodes: op.addOperation(RemoveSceneNodeOperation(node)) group_node = node.getParent() if group_node and group_node.callDecoration( "isGroup") and group_node not in removed_group_nodes: remaining_nodes_in_group = list( set(group_node.getChildren()) - set(nodes)) if len(remaining_nodes_in_group) == 1: removed_group_nodes.append(group_node) op.addOperation( SetParentOperation(remaining_nodes_in_group[0], group_node.getParent())) op.addOperation(RemoveSceneNodeOperation(group_node)) # Reset the print information cura.CuraApplication.CuraApplication.getInstance().getController( ).getScene().sceneChanged.emit(node) op.push()
def mergeWith(self, other): group = GroupedOperation() group.addOperation(self) group.addOperation(other) return group
def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000, add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]: scene_root = Application.getInstance().getController().getScene().getRoot() found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor) not_fit_count = 0 grouped_operation = GroupedOperation() for node, node_item in zip(nodes_to_arrange, node_items): if add_new_nodes_in_scene: grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) if node_item.binId() == 0: # We found a spot for it rotation_matrix = Matrix() rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0)) grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix))) grouped_operation.addOperation(TranslateOperation(node, Vector(node_item.translation().x() / factor, 0, node_item.translation().y() / factor))) else: # We didn't find a spot grouped_operation.addOperation( TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True)) not_fit_count += 1 return grouped_operation, not_fit_count
def _replaceSceneNode(self, existing_node, trimeshes): name = existing_node.getName() file_name = existing_node.getMeshData().getFileName() transformation = existing_node.getWorldTransformation() parent = existing_node.getParent() extruder_id = existing_node.callDecoration("getActiveExtruder") build_plate = existing_node.callDecoration("getBuildPlateNumber") selected = Selection.isSelected(existing_node) op = GroupedOperation() op.addOperation(RemoveSceneNodeOperation(existing_node)) for i, tri_node in enumerate(trimeshes): mesh_data = self._toMeshData(tri_node) new_node = CuraSceneNode() new_node.setSelectable(True) new_node.setMeshData(mesh_data) new_node.setName(name if i == 0 else "%s %d" % (name, i)) new_node.callDecoration("setActiveExtruder", extruder_id) new_node.addDecorator(BuildPlateDecorator(build_plate)) new_node.addDecorator(SliceableObjectDecorator()) op.addOperation(AddSceneNodeOperation(new_node, parent)) op.addOperation( SetTransformMatrixOperation(new_node, transformation)) if selected: Selection.add(new_node) op.push()
def test_addOperationFinalised(): operation_1 = GroupedOperation() operation_2 = GroupedOperation() operation_1.redo() # The operation is now finalized, so it shouldn't be possible to add operations to it now. with pytest.raises(Exception): operation_1.addOperation(operation_2)
def deleteAll(self, only_selectable = True) -> None: Logger.log("i", "Clearing scene") if not self.getController().getToolsEnabled(): return nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if not isinstance(node, SceneNode): continue if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. if only_selectable and not node.isSelectable(): continue if not node.callDecoration("isSliceable") and not node.callDecoration("getLayerData") and not node.callDecoration("isGroup"): continue # Only remove nodes that are selectable. if node.getParent() and cast(SceneNode, node.getParent()).callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: op.addOperation(RemoveSceneNodeOperation(node)) # Reset the print information self.getController().getScene().sceneChanged.emit(node) op.push() Selection.clear()
def resetAll(self): Logger.log("i", "Resetting all scene transformations") nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if type(node) is not SceneNode: continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) if not node.isSelectable(): continue # i.e. node with layer data nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: # Ensure that the object is above the build platform node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) if node.getBoundingBox(): center_y = node.getWorldPosition().y - node.getBoundingBox().bottom else: center_y = 0 op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1))) op.push()
def setExtruderForSelection(self, extruder_id: str) -> None: operation = GroupedOperation() nodes_to_change = [] for node in Selection.getAllSelectedObjects(): # Do not change any nodes that already have the right extruder set. if node.callDecoration("getActiveExtruder") == extruder_id: continue # If the node is a group, apply the active extruder to all children of the group. if node.callDecoration("isGroup"): for grouped_node in BreadthFirstIterator(node): if grouped_node.callDecoration("getActiveExtruder") == extruder_id: continue if grouped_node.callDecoration("isGroup"): continue nodes_to_change.append(grouped_node) continue nodes_to_change.append(node) if not nodes_to_change: # If there are no changes to make, we still need to reset the selected extruders. # This is a workaround for checked menu items being deselected while still being # selected. ExtruderManager.getInstance().resetSelectedObjectExtruders() return for node in nodes_to_change: operation.addOperation(SetObjectExtruderOperation(node, extruder_id)) operation.push()
def removeSelection(self): if not Selection.hasSelection(): return op = GroupedOperation() for node in Selection.getAllSelectedObjects(): op.addOperation(RemoveSceneNodeOperation(node)) op.push() Selection.clear()
def _scaleSelectedNodes(self, scale_vector: Vector) -> None: selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors() if len(selected_nodes) > 1: op = GroupedOperation() for node in selected_nodes: op.addOperation(ScaleOperation(node, scale_vector, scale_around_point=node.getWorldPosition())) op.push() else: for node in selected_nodes: ScaleOperation(node, scale_vector, scale_around_point=node.getWorldPosition()).push()
def centerSelection(self) -> None: operation = GroupedOperation() for node in Selection.getAllSelectedObjects(): current_node = node while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): current_node = current_node.getParent() center_operation = SetTransformOperation(current_node, Vector()) operation.addOperation(center_operation) operation.push()
def setX(self, x): bounding_box = Selection.getBoundingBox() op = GroupedOperation() for selected_node in Selection.getAllSelectedObjects(): world_position = selected_node.getWorldPosition() new_position = world_position.set(x=float(x) + (world_position.x - bounding_box.center.x)) node_op = TranslateOperation(selected_node, new_position, set_position = True) op.addOperation(node_op) op.push() self.operationStopped.emit(self)
def deleteSelection(self): if not self.getController().getToolsEnabled(): return op = GroupedOperation() nodes = Selection.getAllSelectedObjects() for node in nodes: op.addOperation(RemoveSceneNodeOperation(node)) op.push() pass
def run(self): status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0, dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Object")) status_message.show() scene = Application.getInstance().getController().getScene() total_progress = len(self._objects) * self._count current_progress = 0 root = scene.getRoot() arranger = Arrange.create(scene_root=root) nodes = [] for node in self._objects: # If object is part of a group, multiply group current_node = node while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): current_node = current_node.getParent() node_too_big = False if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset) else: node_too_big = True found_solution_for_all = True for i in range(self._count): # We do place the nodes one by one, as we want to yield in between. if not node_too_big: node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr) if node_too_big or not solution_found: found_solution_for_all = False new_location = node.getPosition() new_location = new_location.set(z = 100 - i * 20) node.setPosition(new_location) nodes.append(node) current_progress += 1 status_message.setProgress((current_progress / total_progress) * 100) Job.yieldThread() Job.yieldThread() if nodes: op = GroupedOperation() for new_node in nodes: op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent())) op.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Placing Object")) no_full_solution_message.show()
def multiplyObject(self, object_id, count): node = self.getController().getScene().findObject(object_id) if node: op = GroupedOperation() for i in range(count): new_node = SceneNode() new_node.setMeshData(node.getMeshData()) new_node.setScale(node.getScale()) new_node.translate(Vector((i + 1) * node.getBoundingBox().width, 0, 0)) new_node.setSelectable(True) op.addOperation(AddSceneNodeOperation(new_node, node.getParent())) op.push()
def deleteAll(self): nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if type(node) is not SceneNode or not node.getMeshData(): continue nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: op.addOperation(RemoveSceneNodeOperation(node)) op.push()
def setZ(self, z): bounding_box = Selection.getBoundingBox() op = GroupedOperation() for selected_node in Selection.getAllSelectedObjects(): # Note: The switching of z & y is intentional. We display z as up for the user, # But store the data in openGL space. world_position = selected_node.getWorldPosition() new_position = world_position.set(y=float(z) + (world_position.y - bounding_box.bottom)) node_op = TranslateOperation(selected_node, new_position, set_position = True) op.addOperation(node_op) op.push() self.operationStopped.emit(self)
def setX(self, x): parsed_x = self._parseInt(x) bounding_box = Selection.getBoundingBox() op = GroupedOperation() if not Float.fuzzyCompare(parsed_x, float(bounding_box.center.x), DIMENSION_TOLERANCE): for selected_node in Selection.getAllSelectedObjects(): world_position = selected_node.getWorldPosition() new_position = world_position.set(x=parsed_x + (world_position.x - bounding_box.center.x)) node_op = TranslateOperation(selected_node, new_position, set_position = True) op.addOperation(node_op) op.push() self._controller.toolOperationStopped.emit(self)
def _createEraserMesh(self, parent: CuraSceneNode, position: Vector): node = CuraSceneNode() node.setName("Eraser") node.setSelectable(True) node.setCalculateBoundingBox(True) mesh = self._createCube(10) node.setMeshData(mesh.build()) node.calculateBoundingBoxMesh() active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate node.addDecorator(BuildPlateDecorator(active_build_plate)) node.addDecorator(SliceableObjectDecorator()) stack = node.callDecoration("getStack") # created by SettingOverrideDecorator that is automatically added to CuraSceneNode settings = stack.getTop() definition = stack.getSettingDefinition("anti_overhang_mesh") new_instance = SettingInstance(definition, settings) new_instance.setProperty("value", True) new_instance.resetState() # Ensure that the state is not seen as a user state. settings.addInstance(new_instance) op = GroupedOperation() # First add node to the scene at the correct position/scale, before parenting, so the eraser mesh does not get scaled with the parent op.addOperation(AddSceneNodeOperation(node, self._controller.getScene().getRoot())) op.addOperation(SetParentOperation(node, parent)) op.push() node.setPosition(position, CuraSceneNode.TransformSpace.World) CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)
def multiplyObject(self, object_id, count): node = self.getController().getScene().findObject(object_id) if not node and object_id != 0: #Workaround for tool handles overlapping the selected object node = Selection.getSelectedObject(0) if node: op = GroupedOperation() for i in range(count): if node.getParent() and node.getParent().callDecoration( "isGroup"): new_node = copy.deepcopy( node.getParent()) #Copy the group node. new_node.callDecoration("setConvexHull", None) op.addOperation( AddSceneNodeOperation(new_node, node.getParent().getParent())) else: new_node = copy.deepcopy(node) new_node.callDecoration("setConvexHull", None) op.addOperation( AddSceneNodeOperation(new_node, node.getParent())) op.push()
def resetAll(self): nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if type(node) is not SceneNode or not node.getMeshData(): continue nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: op.addOperation(SetTransformOperation(node, Vector(), Quaternion(), Vector(1, 1, 1))) op.push()
def resetMeshOrigin(self) -> None: nodes_list = self._getSelectedNodes() if not nodes_list: return op = GroupedOperation() for node in nodes_list: mesh_data = node.getMeshData() if not mesh_data: continue extents = mesh_data.getExtents() center = Vector(extents.center.x, extents.center.y, extents.center.z) translation = Matrix() translation.setByTranslation(-center) transformed_mesh_data = mesh_data.getTransformed(translation).set(zero_position=Vector()) new_transformation = Matrix(node.getLocalTransformation().getData()) # Matrix.copy() is not available in Cura 3.5-4.0 new_transformation.translate(center) op.addOperation(SetMeshDataAndNameOperation(node, transformed_mesh_data, node.getName())) op.addOperation(SetTransformMatrixOperation(node, new_transformation)) op.push()
def centerSelection(self) -> None: operation = GroupedOperation() for node in Selection.getAllSelectedObjects(): current_node = node while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): current_node = current_node.getParent() # This was formerly done with SetTransformOperation but because of # unpredictable matrix deconstruction it was possible that mirrors # could manifest as rotations. Centering is therefore done by # moving the node to negative whatever its position is: center_operation = TranslateOperation(current_node, -current_node._position) operation.addOperation(center_operation) operation.push()
def bakeMeshTransformation(self) -> None: nodes_list = self._getSelectedNodes() if not nodes_list: return op = GroupedOperation() for node in nodes_list: mesh_data = node.getMeshData() if not mesh_data: continue mesh_name = node.getName() if not mesh_name: file_name = mesh_data.getFileName() if not file_name: file_name = "" mesh_name = os.path.basename(file_name) if not mesh_name: mesh_name = catalog.i18nc("@text Print job name", "Untitled") local_transformation = node.getLocalTransformation() position = local_transformation.getTranslation() local_transformation.setTranslation(Vector(0,0,0)) transformed_mesh_data = mesh_data.getTransformed(local_transformation) new_transformation = Matrix() new_transformation.setTranslation(position) op.addOperation(SetMeshDataAndNameOperation(node, transformed_mesh_data, mesh_name)) op.addOperation(SetTransformMatrixOperation(node, new_transformation)) op.push()
def setZ(self, z): parsed_z = self._parseInt(z) bounding_box = Selection.getBoundingBox() op = GroupedOperation() if not Float.fuzzyCompare(parsed_z, float(bounding_box.center.y), DIMENSION_TOLERANCE): for selected_node in Selection.getAllSelectedObjects(): # Note: The switching of z & y is intentional. We display z as up for the user, # But store the data in openGL space. world_position = selected_node.getWorldPosition() new_position = world_position.set(y=parsed_z + (world_position.y - bounding_box.bottom)) node_op = TranslateOperation(selected_node, new_position, set_position = True) op.addOperation(node_op) op.push() self._controller.toolOperationStopped.emit(self)
def run(self): status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"), lifetime = 0, dismissable=False, progress = 0) status_message.show() arranger = Arrange.create(fixed_nodes = self._fixed_nodes) # Collect nodes to be placed nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) for node in self._nodes: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset) nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr)) # Sort the nodes with the biggest area first. nodes_arr.sort(key=lambda item: item[0]) nodes_arr.reverse() # Place nodes one at a time start_priority = 0 last_priority = start_priority last_size = None grouped_operation = GroupedOperation() found_solution_for_all = True for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr): # For performance reasons, we assume that when a location does not fit, # it will also not fit for the next object (while what can be untrue). # We also skip possibilities by slicing through the possibilities (step = 10) if last_size == size: # This optimization works if many of the objects have the same size start_priority = last_priority else: start_priority = 0 best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=1) x, y = best_spot.x, best_spot.y node.removeDecorator(ZOffsetDecorator) if node.getBoundingBox(): center_y = node.getWorldPosition().y - node.getBoundingBox().bottom else: center_y = 0 if x is not None: # We could find a place last_size = size last_priority = best_spot.priority arranger.place(x, y, hull_shape_arr) # take place before the next one grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True)) else: Logger.log("d", "Arrange all: could not find spot!") found_solution_for_all = False grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, - idx * 20), set_position = True)) status_message.setProgress((idx + 1) / len(nodes_arr) * 100) Job.yieldThread() grouped_operation.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects")) no_full_solution_message.show()
def ungroupSelected(self): selected_objects = Selection.getAllSelectedObjects().copy() for node in selected_objects: if node.callDecoration("isGroup"): op = GroupedOperation() group_parent = node.getParent() children = node.getChildren().copy() for child in children: # Set the parent of the children to the parent of the group-node op.addOperation(SetParentOperation(child, group_parent)) # Add all individual nodes to the selection Selection.add(child) op.push()
def applyOperation(cls, operation, *args, **kwargs): if not cls.__selection: return op = None if len(cls.__selection) == 1: node = cls.__selection[0] op = operation(node, *args, **kwargs) else: op = GroupedOperation() for node in Selection.getAllSelectedObjects(): op.addOperation(operation(node, *args, **kwargs)) op.push()
def deleteSelection(self): if not self.getController().getToolsEnabled(): return removed_group_nodes = [] op = GroupedOperation() nodes = Selection.getAllSelectedObjects() for node in nodes: op.addOperation(RemoveSceneNodeOperation(node)) group_node = node.getParent() if group_node and group_node.callDecoration("isGroup") and group_node not in removed_group_nodes: remaining_nodes_in_group = list(set(group_node.getChildren()) - set(nodes)) if len(remaining_nodes_in_group) == 1: removed_group_nodes.append(group_node) op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent())) op.addOperation(RemoveSceneNodeOperation(group_node)) op.push()
def multiplyObject(self, object_id, count): node = self.getController().getScene().findObject(object_id) if not node and object_id != 0: # Workaround for tool handles overlapping the selected object node = Selection.getSelectedObject(0) if node: current_node = node # Find the topmost group while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): current_node = current_node.getParent() op = GroupedOperation() for _ in range(count): new_node = copy.deepcopy(current_node) op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent())) op.push()
def deleteAll(self): nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if type(node) is not SceneNode: continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: op.addOperation(RemoveSceneNodeOperation(node)) op.push()
def event(self, event): super().event(event) if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): # Initialise a mirror operation if MouseEvent.LeftButton not in event.buttons: return False id = self._selection_pass.getIdAtPosition(event.x, event.y) if not id: return False if ToolHandle.isAxis(id): self.setLockedAxis(id) self._operation_started = True self.operationStarted.emit(self) return True if event.type == Event.MouseReleaseEvent: if self._operation_started: self._operation_started = False self.operationStopped.emit(self) # Perform a mirror operation if self.getLockedAxis(): op = None if Selection.getCount() == 1: node = Selection.getSelectedObject(0) if self.getLockedAxis() == ToolHandle.XAxis: mirror = Vector(-1, 1, 1) elif self.getLockedAxis() == ToolHandle.YAxis: mirror = Vector(1, -1, 1) elif self.getLockedAxis() == ToolHandle.ZAxis: mirror = Vector(1, 1, -1) else: mirror = Vector(1, 1, 1) op = MirrorOperation(node, mirror, mirror_around_center = True) else: op = GroupedOperation() for node in Selection.getAllSelectedObjects(): if self.getLockedAxis() == ToolHandle.XAxis: mirror = Vector(-1, 1, 1) elif self.getLockedAxis() == ToolHandle.YAxis: mirror = Vector(1, -1, 1) elif self.getLockedAxis() == ToolHandle.ZAxis: mirror = Vector(1, 1, -1) else: mirror = Vector(1, 1, 1) op.addOperation(MirrorOperation(node, mirror, mirror_around_center = True)) op.push() self.setLockedAxis(None) return True return False
def multiplyObject(self, object_id, count): node = self.getController().getScene().findObject(object_id) if not node and object_id != 0: #Workaround for tool handles overlapping the selected object node = Selection.getSelectedObject(0) if node: op = GroupedOperation() for i in range(count): new_node = SceneNode() new_node.setMeshData(node.getMeshData()) new_node.translate(Vector((i + 1) * node.getBoundingBox().width, node.getPosition().y, 0)) new_node.setOrientation(node.getOrientation()) new_node.setScale(node.getScale()) new_node.setSelectable(True) op.addOperation(AddSceneNodeOperation(new_node, node.getParent())) op.push()
def test_addOperationFinalised(): operation_1 = GroupedOperation() operation_2 = GroupedOperation() operation_1.redo( ) # The operation is now finalized, so it shouldn't be possible to add operations to it now. with pytest.raises(Exception): operation_1.addOperation(operation_2)
def run(self): status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0, dismissable=False, progress=0) status_message.show() scene = Application.getInstance().getController().getScene() node = scene.findObject(self._object_id) if not node and self._object_id != 0: # Workaround for tool handles overlapping the selected object node = Selection.getSelectedObject(0) # If object is part of a group, multiply group current_node = node while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): current_node = current_node.getParent() root = scene.getRoot() arranger = Arrange.create(scene_root=root) offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset) nodes = [] found_solution_for_all = True for i in range(self._count): # We do place the nodes one by one, as we want to yield in between. node, solution_found = arranger.findNodePlacement(current_node, offset_shape_arr, hull_shape_arr) if not solution_found: found_solution_for_all = False new_location = node.getPosition() new_location = new_location.set(z = 100 - i * 20) node.setPosition(new_location) nodes.append(node) Job.yieldThread() status_message.setProgress((i + 1) / self._count * 100) if nodes: op = GroupedOperation() for new_node in nodes: op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent())) op.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects")) no_full_solution_message.show()
def resetAllTranslation(self): nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if type(node) is not SceneNode: continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) op.addOperation(SetTransformOperation(node, Vector(0, 0, 0))) op.push()
def run(self): status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"), lifetime = 0, dismissable=False, progress = 0) status_message.show() arranger = Arrange.create(fixed_nodes = self._fixed_nodes) # Collect nodes to be placed nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) for node in self._nodes: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset) nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr)) # Sort the nodes with the biggest area first. nodes_arr.sort(key=lambda item: item[0]) nodes_arr.reverse() # Place nodes one at a time start_priority = 0 last_priority = start_priority last_size = None grouped_operation = GroupedOperation() found_solution_for_all = True for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr): # For performance reasons, we assume that when a location does not fit, # it will also not fit for the next object (while what can be untrue). # We also skip possibilities by slicing through the possibilities (step = 10) if last_size == size: # This optimization works if many of the objects have the same size start_priority = last_priority else: start_priority = 0 best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10) x, y = best_spot.x, best_spot.y node.removeDecorator(ZOffsetDecorator) if node.getBoundingBox(): center_y = node.getWorldPosition().y - node.getBoundingBox().bottom else: center_y = 0 if x is not None: # We could find a place last_size = size last_priority = best_spot.priority arranger.place(x, y, hull_shape_arr) # take place before the next one grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True)) else: Logger.log("d", "Arrange all: could not find spot!") found_solution_for_all = False grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, - idx * 20), set_position = True)) status_message.setProgress((idx + 1) / len(nodes_arr) * 100) Job.yieldThread() grouped_operation.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects")) no_full_solution_message.show()
def resetAll(self): nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if type(node) is not SceneNode: continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue #Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue #Grouped nodes don't need resetting as their parent (the group) is resetted) nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: # Ensure that the object is above the build platform op.addOperation(SetTransformOperation(node, Vector(0,0,0), Quaternion(), Vector(1, 1, 1))) op.push()
def applyOperation(cls, operation, *args, **kwargs): if not cls.__selection: return operations = [] if len(cls.__selection) == 1: node = cls.__selection[0] op = operation(node, *args, **kwargs) operations.append(op) else: op = GroupedOperation() for node in Selection.getAllSelectedObjects(): sub_op = operation(node, *args, **kwargs) op.addOperation(sub_op) operations.append(sub_op) op.push() return operations
def deleteObject(self, object_id): if not self.getController().getToolsEnabled(): return node = self.getController().getScene().findObject(object_id) if not node and object_id != 0: # Workaround for tool handles overlapping the selected object node = Selection.getSelectedObject(0) if node: op = GroupedOperation() op.addOperation(RemoveSceneNodeOperation(node)) group_node = node.getParent() if group_node: # Note that at this point the node has not yet been deleted if len(group_node.getChildren()) <= 2 and group_node.callDecoration("isGroup"): op.addOperation(SetParentOperation(group_node.getChildren()[0], group_node.getParent())) op.addOperation(RemoveSceneNodeOperation(group_node)) op.push()
def resetAll(self): Logger.log("i", "Resetting all scene transformations") nodes = [] for node in DepthFirstIterator( self.getController().getScene().getRoot()): if type(node) is not SceneNode: continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: # Ensure that the object is above the build platform node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator) center_y = 0 if node.callDecoration("isGroup"): center_y = node.getWorldPosition().y - node.getBoundingBox( ).bottom else: center_y = node.getMeshData().getCenterPosition().y op.addOperation( SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1))) op.push()
def setY(self, y: str) -> None: """Set the y-location of the selected object(s) by translating relative to the selection bounding box center. :param y: Location in mm. """ parsed_y = self._parseFloat(y) bounding_box = Selection.getBoundingBox() if not Float.fuzzyCompare(parsed_y, float(bounding_box.center.z), DIMENSION_TOLERANCE): selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors() if len(selected_nodes) > 1: op = GroupedOperation() for selected_node in selected_nodes: # Note; The switching of z & y is intentional. We display z as up for the user, # But store the data in openGL space. world_position = selected_node.getWorldPosition() new_position = world_position.set(z = parsed_y + (world_position.z - bounding_box.center.z)) node_op = TranslateOperation(selected_node, new_position, set_position = True) op.addOperation(node_op) op.push() else: for selected_node in selected_nodes: world_position = selected_node.getWorldPosition() new_position = world_position.set(z = parsed_y + (world_position.z - bounding_box.center.z)) TranslateOperation(selected_node, new_position, set_position = True).push() self._controller.toolOperationStopped.emit(self)
def mergeWith(self, other): group = GroupedOperation() group.addOperation(other) group.addOperation(self) return group
def deleteAllNodesWithMeshData(self, only_selectable:bool = True) -> None: Logger.log("i", "Clearing scene") if not self.getToolsEnabled(): return nodes = [] for node in DepthFirstIterator(self.getScene().getRoot()): if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. if only_selectable and not node.isSelectable(): continue # Only remove nodes that are selectable. if node.getParent() and cast(SceneNode, node.getParent()).callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) nodes.append(node) if nodes: from UM.Operations.GroupedOperation import GroupedOperation op = GroupedOperation() for node in nodes: from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation op.addOperation(RemoveSceneNodeOperation(node)) # Reset the print information self.getScene().sceneChanged.emit(node) op.push() from UM.Scene.Selection import Selection Selection.clear()
def setBuildPlateForSelection(self, build_plate_nr: int) -> None: Logger.log("d", "Setting build plate number... %d" % build_plate_nr) operation = GroupedOperation() root = cura.CuraApplication.CuraApplication.getInstance( ).getController().getScene().getRoot() nodes_to_change = [] for node in Selection.getAllSelectedObjects(): parent_node = node # Find the parent node to change instead while parent_node.getParent() != root: parent_node = parent_node.getParent() for single_node in BreadthFirstIterator( parent_node ): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. nodes_to_change.append(single_node) if not nodes_to_change: Logger.log("d", "Nothing to change.") return for node in nodes_to_change: operation.addOperation( SetBuildPlateNumberOperation(node, build_plate_nr)) operation.push() Selection.clear()
def deleteAll(self, only_selectable=True) -> None: Logger.log("i", "Clearing scene") if not self.getController().getToolsEnabled(): return nodes = [] for node in DepthFirstIterator( self.getController().getScene().getRoot() ): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if not isinstance(node, SceneNode): continue if (not node.getMeshData() and not node.callDecoration("getLayerData") ) and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. if only_selectable and not node.isSelectable(): continue if not node.callDecoration( "isSliceable") and not node.callDecoration( "getLayerData") and not node.callDecoration("isGroup"): continue # Only remove nodes that are selectable. if node.getParent() and node.getParent().callDecoration("isGroup"): continue # Grouped nodes don't need resetting as their parent (the group) is resetted) nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: op.addOperation(RemoveSceneNodeOperation(node)) # Reset the print information self.getController().getScene().sceneChanged.emit(node) op.push() Selection.clear()
def setZ(self, z: str) -> None: parsed_z = self._parseFloat(z) bounding_box = Selection.getBoundingBox() if not Float.fuzzyCompare(parsed_z, float(bounding_box.bottom), DIMENSION_TOLERANCE): selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors() if len(selected_nodes) > 1: op = GroupedOperation() for selected_node in selected_nodes: # Note: The switching of z & y is intentional. We display z as up for the user, # But store the data in openGL space. world_position = selected_node.getWorldPosition() new_position = world_position.set( y=parsed_z + (world_position.y - bounding_box.bottom)) node_op = TranslateOperation(selected_node, new_position, set_position=True) op.addOperation(node_op) op.push() else: for selected_node in selected_nodes: world_position = selected_node.getWorldPosition() new_position = world_position.set( y=parsed_z + (world_position.y - bounding_box.bottom)) TranslateOperation(selected_node, new_position, set_position=True).push() self._controller.toolOperationStopped.emit(self)
def setX(self, x: str) -> None: parsed_x = self._parseFloat(x) bounding_box = Selection.getBoundingBox() if not Float.fuzzyCompare(parsed_x, float(bounding_box.center.x), DIMENSION_TOLERANCE): selected_nodes = self._getSelectedObjectsWithoutSelectedAncestors() if len(selected_nodes) > 1: op = GroupedOperation() for selected_node in self._getSelectedObjectsWithoutSelectedAncestors( ): world_position = selected_node.getWorldPosition() new_position = world_position.set( x=parsed_x + (world_position.x - bounding_box.center.x)) node_op = TranslateOperation(selected_node, new_position, set_position=True) op.addOperation(node_op) op.push() else: for selected_node in self._getSelectedObjectsWithoutSelectedAncestors( ): world_position = selected_node.getWorldPosition() new_position = world_position.set( x=parsed_x + (world_position.x - bounding_box.center.x)) TranslateOperation(selected_node, new_position, set_position=True).push() self._controller.toolOperationStopped.emit(self)
def applyOperation(cls, operation, *args, **kwargs): """Apply an operation to the entire selection This will create and push an operation onto the operation stack. Dependent on whether there is one item selected or multiple it will be just the operation or a grouped operation containing the operation for each selected node. :param operation: :type{Class} The operation to create and push. It should take a SceneNode as first positional parameter. :param args: The additional positional arguments passed along to the operation constructor. :param kwargs: The additional keyword arguments that will be passed along to the operation constructor. :return: list of instantiated operations """ if not cls.__selection: return operations = [] if len(cls.__selection) == 1: node = cls.__selection[0] op = operation(node, *args, **kwargs) operations.append(op) else: op = GroupedOperation() for node in Selection.getAllSelectedObjects(): sub_op = operation(node, *args, **kwargs) op.addOperation(sub_op) operations.append(sub_op) op.push() return operations
def run(self): status_message = Message(i18n_catalog.i18nc( "@info:status", "Multiplying and placing objects"), lifetime=0, dismissable=False, progress=0) status_message.show() scene = Application.getInstance().getController().getScene() node = scene.findObject(self._object_id) if not node and self._object_id != 0: # Workaround for tool handles overlapping the selected object node = Selection.getSelectedObject(0) # If object is part of a group, multiply group current_node = node while current_node.getParent() and current_node.getParent( ).callDecoration("isGroup"): current_node = current_node.getParent() root = scene.getRoot() arranger = Arrange.create(scene_root=root) node_too_big = False if node.getBoundingBox().width < 300 or node.getBoundingBox( ).depth < 300: offset_shape_arr, hull_shape_arr = ShapeArray.fromNode( current_node, min_offset=self._min_offset) else: node_too_big = True nodes = [] found_solution_for_all = True for i in range(self._count): # We do place the nodes one by one, as we want to yield in between. if not node_too_big: node, solution_found = arranger.findNodePlacement( current_node, offset_shape_arr, hull_shape_arr) if node_too_big or not solution_found: found_solution_for_all = False new_location = node.getPosition() new_location = new_location.set(z=100 - i * 20) node.setPosition(new_location) nodes.append(node) Job.yieldThread() status_message.setProgress((i + 1) / self._count * 100) if nodes: op = GroupedOperation() for new_node in nodes: op.addOperation( AddSceneNodeOperation(new_node, current_node.getParent())) op.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message( i18n_catalog.i18nc( "@info:status", "Unable to find a location within the build volume for all objects" )) no_full_solution_message.show()
def multiplySelection(self, count: int) -> None: application = cura.CuraApplication.CuraApplication.getInstance() global_container_stack = application.getGlobalContainerStack() if not global_container_stack: return definition_container = global_container_stack.getBottom() if definition_container and definition_container.getId() not in [ "blackbelt", "blackbeltvd" ]: # for all other printers do the normal multiply/arrange super().multiplySelection(count) return scene_root = application.getController().getScene().getRoot() current_nodes = [] for node in DepthFirstIterator( scene_root ): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.callDecoration("isSliceable") or node.callDecoration( "isGroup"): current_nodes.append(node) new_nodes = [] # type: List[CuraSceneNode] processed_nodes = [] # type: List[CuraSceneNode] active_build_plate = application.getMultiBuildPlateModel( ).activeBuildPlate for node in Selection.getAllSelectedObjects()[:]: # If object is part of a group, multiply group while node.getParent() and ( node.getParent().callDecoration("isGroup") or node.getParent().callDecoration("isSliceable")): node = node.getParent() if node in processed_nodes: continue processed_nodes.append(node) for i in range(count): new_node = copy.deepcopy(node) new_node.callDecoration("setBuildPlateNumber", active_build_plate) for child in new_node.getChildren(): child.callDecoration("setBuildPlateNumber", active_build_plate) new_nodes.append(new_node) if new_nodes: op = GroupedOperation() for new_node in new_nodes: op.addOperation(AddSceneNodeOperation(new_node, scene_root)) op.push() application.arrange(new_nodes, current_nodes)
def setExtruderForSelection(self, extruder_id: str) -> None: operation = GroupedOperation() nodes_to_change = [] for node in Selection.getAllSelectedObjects(): # If the node is a group, apply the active extruder to all children of the group. if node.callDecoration("isGroup"): for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if grouped_node.callDecoration("getActiveExtruder") == extruder_id: continue if grouped_node.callDecoration("isGroup"): continue nodes_to_change.append(grouped_node) continue # Do not change any nodes that already have the right extruder set. if node.callDecoration("getActiveExtruder") == extruder_id: continue nodes_to_change.append(node) if not nodes_to_change: # If there are no changes to make, we still need to reset the selected extruders. # This is a workaround for checked menu items being deselected while still being # selected. ExtruderManager.getInstance().resetSelectedObjectExtruders() return for node in nodes_to_change: operation.addOperation(SetObjectExtruderOperation(node, extruder_id)) operation.push()
def _onSelectedFaceChanged(self): self._handle.setEnabled(not Selection.getFaceSelectMode()) selected_face = Selection.getSelectedFace() if not Selection.getSelectedFace() or not (Selection.hasSelection() and Selection.getFaceSelectMode()): return original_node, face_id = selected_face meshdata = original_node.getMeshDataTransformed() if not meshdata or face_id < 0: return rotation_point, face_normal = meshdata.getFacePlane(face_id) rotation_point_vector = Vector(rotation_point[0], rotation_point[1], rotation_point[2]) face_normal_vector = Vector(face_normal[0], face_normal[1], face_normal[2]) rotation_quaternion = Quaternion.rotationTo(face_normal_vector.normalized(), Vector(0.0, -1.0, 0.0)) operation = GroupedOperation() current_node = None # type: Optional[SceneNode] for node in Selection.getAllSelectedObjects(): current_node = node parent_node = current_node.getParent() while parent_node and parent_node.callDecoration("isGroup"): current_node = parent_node parent_node = current_node.getParent() if current_node is None: return rotate_operation = RotateOperation(current_node, rotation_quaternion, rotation_point_vector) operation.addOperation(rotate_operation) operation.push()
def setZ(self, Z): if float(Z) != self._Z_angle: self._angle = ((float(Z) % 360) - (self._Z_angle % 360)) % 360 self._Z_angle = float(Z) #rotation = Quaternion.fromAngleAxis(math.radians( self._angle ), Vector.Unit_Z) rotation = Quaternion() rotation.setByAngleAxis(math.radians(self._angle), Vector.Unit_Z) # Save the current positions of the node, as we want to rotate around their current centres self._saved_node_positions = [] for node in Selection.getAllSelectedObjects(): self._saved_node_positions.append((node, node.getPosition())) node._rotationZ = self._Z_angle # Rate-limit the angle change notification # This is done to prevent the UI from being flooded with property change notifications, # which in turn would trigger constant repaints. new_time = time.monotonic() if not self._angle_update_time or new_time - self._angle_update_time > 0.1: self._angle_update_time = new_time # Rotate around the saved centeres of all selected nodes op = GroupedOperation() for node, position in self._saved_node_positions: op.addOperation( RotateOperation(node, rotation, rotate_around_point=position)) op.push() self._angle = 0 self.propertyChanged.emit()
def resetAll(self): nodes = [] for node in DepthFirstIterator( self.getController().getScene().getRoot()): if type(node) is not SceneNode: continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue #Node that doesnt have a mesh and is not a group. if node.getParent() and node.getParent().callDecoration("isGroup"): continue #Grouped nodes don't need resetting as their parent (the group) is resetted) nodes.append(node) if nodes: op = GroupedOperation() for node in nodes: # Ensure that the object is above the build platform move_distance = node.getBoundingBox().center.y if move_distance <= 0: move_distance = -node.getBoundingBox().bottom op.addOperation( SetTransformOperation(node, Vector(0, move_distance, 0), Quaternion(), Vector(1, 1, 1))) op.push()
def centerSelection(self) -> None: operation = GroupedOperation() for node in Selection.getAllSelectedObjects(): current_node = node while current_node.getParent() and current_node.getParent( ).callDecoration("isGroup"): current_node = current_node.getParent() vector = Vector() print_mode_enabled = Application.getInstance( ).getGlobalContainerStack().getProperty("print_mode", "enabled") if print_mode_enabled: print_mode = Application.getInstance().getGlobalContainerStack( ).getProperty("print_mode", "value") if print_mode != "regular": machine_width = Application.getInstance( ).getGlobalContainerStack().getProperty( "machine_width", "value") center = -machine_width / 4 if print_mode == "mirror": machine_head_with_fans_polygon = Application.getInstance( ).getGlobalContainerStack().getProperty( "machine_head_with_fans_polygon", "value") machine_head_size = math.fabs( machine_head_with_fans_polygon[0][0] - machine_head_with_fans_polygon[2][0]) center -= machine_head_size / 4 vector = Vector(center, 0, 0) center_operation = SetTransformOperation(current_node, vector) operation.addOperation(center_operation) operation.push()
def test_push(): operation_stack = OperationStack(MagicMock()) operation_stack.changed.emit = MagicMock() test_operation = GroupedOperation() test_operation_2 = GroupedOperation() operation_stack.push(test_operation) operation_stack.push(test_operation_2) # Since we added two operations that can be merged, we should end up with one operation! assert len(operation_stack.getOperations()) == 1 test_operation_3 = GroupedOperation() # Fake call to notify the operation stack that the tool has stopped doing something operation_stack._onToolOperationStopped(None) # Pretend like another operation was added but with a lot of time in between. test_operation_3._timestamp = time.time() + 2000000 operation_stack.push(test_operation_3) assert len(operation_stack.getOperations()) == 2 operation_stack.undo() # The count should be at 4, since we added 3 operations and then undid the last one. assert operation_stack.changed.emit.call_count == 4 operation_stack.undo() assert operation_stack.changed.emit.call_count == 5 # There is nothing to undo! assert not operation_stack.canUndo() operation_stack.undo() assert operation_stack.changed.emit.call_count == 5 operation_stack.redo() assert operation_stack.changed.emit.call_count == 6 operation_stack.redo() assert operation_stack.changed.emit.call_count == 7 assert not operation_stack.canRedo()
def event(self, event): super().event(event) if event.type == Event.MousePressEvent: if MouseEvent.LeftButton not in event.buttons: return False id = self._renderer.getIdAtCoordinate(event.x, event.y) if not id: return False if ToolHandle.isAxis(id): self.setLockedAxis(id) return True if event.type == Event.MouseReleaseEvent: if self.getLockedAxis(): op = None if Selection.getCount() == 1: node = Selection.getSelectedObject(0) scale = node.getScale() if self.getLockedAxis() == ToolHandle.XAxis: scale.setX(-scale.x) elif self.getLockedAxis() == ToolHandle.YAxis: scale.setY(-scale.y) elif self.getLockedAxis() == ToolHandle.ZAxis: scale.setZ(-scale.z) op = ScaleOperation(node, scale, set_scale=True) else: op = GroupedOperation() for node in Selection.getAllSelectedObjects(): scale = node.getScale() if self.getLockedAxis() == ToolHandle.XAxis: scale.setX(-scale.x) elif self.getLockedAxis() == ToolHandle.YAxis: scale.setY(-scale.y) elif self.getLockedAxis() == ToolHandle.ZAxis: scale.setZ(-scale.z) op.addOperation( ScaleOperation(node, scale, set_scale=True)) op.push() self.setLockedAxis(None) return True return False