From 4f9d041ae84206c4ad3f7d2c6f01912965d57890 Mon Sep 17 00:00:00 2001 From: "c.lamboo" Date: Thu, 11 May 2023 16:14:38 +0200 Subject: [PATCH] Add rotation lock in arrange and multiply objects Add rotation lock in - context menu item arrange and - checkbox in multiply objects dialog CURA-7951 --- cura/Arranging/ArrangeObjectsJob.py | 7 +++-- cura/Arranging/Nest2DArrange.py | 36 ++++++++++++++++------- cura/CuraActions.py | 10 ++++--- cura/CuraApplication.py | 11 ++++---- cura/MultiplyObjectsJob.py | 14 +++++---- resources/qml/Actions.qml | 20 +++++++++++-- resources/qml/Menus/ContextMenu.qml | 44 ++++++++++++++++++----------- 7 files changed, 97 insertions(+), 45 deletions(-) diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 6ba6717191..f938b44d1f 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -14,11 +14,13 @@ i18n_catalog = i18nCatalog("cura") class ArrangeObjectsJob(Job): - def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8) -> None: + def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset=8, + lock_rotation: bool = False) -> None: super().__init__() self._nodes = nodes self._fixed_nodes = fixed_nodes self._min_offset = min_offset + self._lock_rotation = lock_rotation def run(self): found_solution_for_all = False @@ -30,7 +32,8 @@ class ArrangeObjectsJob(Job): status_message.show() try: - found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes) + found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes, + lock_rotation=self._lock_rotation) except: # If the thread crashes, the message should still close Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.") diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index 21427f1194..8921c9ede2 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -22,7 +22,13 @@ if TYPE_CHECKING: from cura.BuildVolume import BuildVolume -def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000) -> Tuple[bool, List[Item]]: +def findNodePlacement( + nodes_to_arrange: List["SceneNode"], + build_volume: "BuildVolume", + fixed_nodes: Optional[List["SceneNode"]] = None, + factor: int = 10000, + lock_rotation: bool = False +) -> Tuple[bool, List[Item]]: """ Find placement for a set of scene nodes, but don't actually move them just yet. :param nodes_to_arrange: The list of nodes that need to be moved. @@ -30,6 +36,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV :param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes are placed. :param factor: The library that we use is int based. This factor defines how accurate we want it to be. + :param lock_rotation: If set to true the orientation of the object will remain the same :return: tuple (found_solution_for_all, node_items) WHERE @@ -100,6 +107,8 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV config = NfpConfig() config.accuracy = 1.0 config.alignment = NfpConfig.Alignment.DONT_ALIGN + if lock_rotation: + config.rotations = [0.0] num_bins = nest(node_items, build_plate_bounding_box, spacing, config) @@ -114,10 +123,12 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV 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]: + factor: int = 10000, + add_new_nodes_in_scene: bool = False, + lock_rotation: 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) + found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor, + lock_rotation) not_fit_count = 0 grouped_operation = GroupedOperation() @@ -141,11 +152,14 @@ def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"], return grouped_operation, not_fit_count -def arrange(nodes_to_arrange: List["SceneNode"], - build_volume: "BuildVolume", - fixed_nodes: Optional[List["SceneNode"]] = None, - factor = 10000, - add_new_nodes_in_scene: bool = False) -> bool: +def arrange( + nodes_to_arrange: List["SceneNode"], + build_volume: "BuildVolume", + fixed_nodes: Optional[List["SceneNode"]] = None, + factor=10000, + add_new_nodes_in_scene: bool = False, + lock_rotation: bool = False +) -> bool: """ Find placement for a set of scene nodes, and move them by using a single grouped operation. :param nodes_to_arrange: The list of nodes that need to be moved. @@ -154,10 +168,12 @@ def arrange(nodes_to_arrange: List["SceneNode"], are placed. :param factor: The library that we use is int based. This factor defines how accuracte we want it to be. :param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations + :param lock_rotation: If set to true the orientation of the object will remain the same :return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects """ - grouped_operation, not_fit_count = createGroupOperationForArrange(nodes_to_arrange, build_volume, fixed_nodes, factor, add_new_nodes_in_scene) + grouped_operation, not_fit_count = createGroupOperationForArrange(nodes_to_arrange, build_volume, fixed_nodes, + factor, add_new_nodes_in_scene, lock_rotation) grouped_operation.push() return not_fit_count == 0 diff --git a/cura/CuraActions.py b/cura/CuraActions.py index 193803325f..fed57c8500 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -75,19 +75,21 @@ class CuraActions(QObject): center_y = 0 # Move the object so that it's bottom is on to of the buildplate - center_operation = TranslateOperation(current_node, Vector(0, center_y, 0), set_position = True) + center_operation = TranslateOperation(current_node, Vector(0, center_y, 0), set_position=True) operation.addOperation(center_operation) operation.push() - @pyqtSlot(int) - def multiplySelection(self, count: int) -> None: + @pyqtSlot(int, bool) + def multiplySelection(self, count: int, lock_rotation: bool) -> None: """Multiply all objects in the selection :param count: The number of times to multiply the selection. + :param lock_rotation: If set to true the orientation of the object will remain the same """ min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors - job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8)) + job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset=max(min_offset, 8), + lock_rotation=lock_rotation) job.start() @pyqtSlot() diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 113e7b3ff4..15e7a60fcb 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1421,8 +1421,8 @@ class CuraApplication(QtApplication): op.push() # Single build plate - @pyqtSlot() - def arrangeAll(self) -> None: + @pyqtSlot(bool) + def arrangeAll(self, lock_rotation: bool) -> None: nodes_to_arrange = [] active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate locked_nodes = [] @@ -1452,17 +1452,18 @@ class CuraApplication(QtApplication): locked_nodes.append(node) else: nodes_to_arrange.append(node) - self.arrange(nodes_to_arrange, locked_nodes) + self.arrange(nodes_to_arrange, locked_nodes, lock_rotation) - def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None: + def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], lock_rotation: bool = False) -> None: """Arrange a set of nodes given a set of fixed nodes :param nodes: nodes that we have to place :param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes + :param lock_rotation: If set to true the orientation of the object will remain the same """ min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors - job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8)) + job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset=max(min_offset, 8), lock_rotation=lock_rotation) job.start() @pyqtSlot() diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 1446ae687e..ff4f362b4c 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -20,11 +20,12 @@ i18n_catalog = i18nCatalog("cura") class MultiplyObjectsJob(Job): - def __init__(self, objects, count, min_offset = 8): + def __init__(self, objects, count: int, min_offset: int = 8, lock_rotation: bool = False): super().__init__() self._objects = objects - self._count = count - self._min_offset = min_offset + self._count: int = count + self._min_offset: int = min_offset + self._lock_rotation: bool = lock_rotation def run(self) -> None: status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime = 0, @@ -39,7 +40,7 @@ class MultiplyObjectsJob(Job): root = scene.getRoot() - processed_nodes = [] # type: List[SceneNode] + processed_nodes: List[SceneNode] = [] nodes = [] fixed_nodes = [] @@ -79,8 +80,9 @@ class MultiplyObjectsJob(Job): group_operation, not_fit_count = createGroupOperationForArrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, - factor = 10000, - add_new_nodes_in_scene = True) + factor=10000, + add_new_nodes_in_scene=True, + lock_rotation=self._lock_rotation) found_solution_for_all = not_fit_count == 0 if nodes_to_add_without_arrange: diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index 6cd75b51ac..eb9f1c7f21 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -41,7 +41,9 @@ Item property alias deleteAll: deleteAllAction property alias reloadAll: reloadAllAction property alias arrangeAll: arrangeAllAction + property alias arrangeAllLock: arrangeAllLockAction property alias arrangeSelection: arrangeSelectionAction + property alias arrangeSelectionLock: arrangeSelectionLockAction property alias resetAllTranslation: resetAllTranslationAction property alias resetAll: resetAllAction @@ -412,15 +414,29 @@ Item { id: arrangeAllAction text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models") - onTriggered: Printer.arrangeAll() + onTriggered: Printer.arrangeAll(false) shortcut: "Ctrl+R" } + Action + { + id: arrangeAllLockAction + text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models Without Rotation") + onTriggered: Printer.arrangeAll(true) + } + Action { id: arrangeSelectionAction text: catalog.i18nc("@action:inmenu menubar:edit","Arrange Selection") - onTriggered: Printer.arrangeSelection() + onTriggered: Printer.arrangeSelection(false) + } + + Action + { + id: arrangeSelectionLockAction + text: catalog.i18nc("@action:inmenu menubar:edit","Arrange Selection Without Rotation") + onTriggered: Printer.arrangeSelection(true) } Action diff --git a/resources/qml/Menus/ContextMenu.qml b/resources/qml/Menus/ContextMenu.qml index 65f3409c8a..c327a37a2b 100644 --- a/resources/qml/Menus/ContextMenu.qml +++ b/resources/qml/Menus/ContextMenu.qml @@ -53,6 +53,7 @@ Cura.Menu Cura.MenuSeparator {} Cura.MenuItem { action: Cura.Actions.selectAll } Cura.MenuItem { action: Cura.Actions.arrangeAll } + Cura.MenuItem { action: Cura.Actions.arrangeAllLock } Cura.MenuItem { action: Cura.Actions.deleteAll } Cura.MenuItem { action: Cura.Actions.reloadAll } Cura.MenuItem { action: Cura.Actions.resetAllTranslation } @@ -96,7 +97,7 @@ Cura.Menu minimumWidth: UM.Theme.getSize("small_popup_dialog").width minimumHeight: UM.Theme.getSize("small_popup_dialog").height - onAccepted: CuraActions.multiplySelection(copiesField.value) + onAccepted: CuraActions.multiplySelection(copiesField.value, lockRotationField.checked) buttonSpacing: UM.Theme.getSize("thin_margin").width @@ -114,27 +115,38 @@ Cura.Menu } ] - Row + Column { - spacing: UM.Theme.getSize("default_margin").width + spacing: UM.Theme.getSize("default_margin").height - UM.Label + Row { - text: catalog.i18nc("@label", "Number of Copies") - anchors.verticalCenter: copiesField.verticalCenter - width: contentWidth - wrapMode: Text.NoWrap + spacing: UM.Theme.getSize("default_margin").width + + UM.Label + { + text: catalog.i18nc("@label", "Number of Copies") + anchors.verticalCenter: copiesField.verticalCenter + width: contentWidth + wrapMode: Text.NoWrap + } + + Cura.SpinBox + { + id: copiesField + editable: true + focus: true + from: 1 + to: 99 + width: 2 * UM.Theme.getSize("button").width + value: 1 + } } - Cura.SpinBox + UM.CheckBox { - id: copiesField - editable: true - focus: true - from: 1 - to: 99 - width: 2 * UM.Theme.getSize("button").width - value: 1 + id: lockRotationField + text: catalog.i18nc("@label", "Lock Rotation") } } }