Add rotation lock in arrange and multiply objects

Add rotation lock in
- context menu item arrange and
- checkbox in multiply objects dialog

CURA-7951
This commit is contained in:
c.lamboo 2023-05-11 16:14:38 +02:00
parent 6a5a9e70af
commit 4f9d041ae8
7 changed files with 97 additions and 45 deletions

View file

@ -14,11 +14,13 @@ i18n_catalog = i18nCatalog("cura")
class ArrangeObjectsJob(Job): 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__() super().__init__()
self._nodes = nodes self._nodes = nodes
self._fixed_nodes = fixed_nodes self._fixed_nodes = fixed_nodes
self._min_offset = min_offset self._min_offset = min_offset
self._lock_rotation = lock_rotation
def run(self): def run(self):
found_solution_for_all = False found_solution_for_all = False
@ -30,7 +32,8 @@ class ArrangeObjectsJob(Job):
status_message.show() status_message.show()
try: 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 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.") Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.")

View file

@ -22,7 +22,13 @@ if TYPE_CHECKING:
from cura.BuildVolume import BuildVolume 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. 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. :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 :param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
are placed. are placed.
:param factor: The library that we use is int based. This factor defines how accurate we want it to be. :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) :return: tuple (found_solution_for_all, node_items)
WHERE WHERE
@ -100,6 +107,8 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV
config = NfpConfig() config = NfpConfig()
config.accuracy = 1.0 config.accuracy = 1.0
config.alignment = NfpConfig.Alignment.DONT_ALIGN config.alignment = NfpConfig.Alignment.DONT_ALIGN
if lock_rotation:
config.rotations = [0.0]
num_bins = nest(node_items, build_plate_bounding_box, spacing, config) 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"], def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"],
build_volume: "BuildVolume", build_volume: "BuildVolume",
fixed_nodes: Optional[List["SceneNode"]] = None, fixed_nodes: Optional[List["SceneNode"]] = None,
factor = 10000, factor: int = 10000,
add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]: add_new_nodes_in_scene: bool = False,
lock_rotation: bool = False) -> Tuple[GroupedOperation, int]:
scene_root = Application.getInstance().getController().getScene().getRoot() 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 not_fit_count = 0
grouped_operation = GroupedOperation() grouped_operation = GroupedOperation()
@ -141,11 +152,14 @@ def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"],
return grouped_operation, not_fit_count return grouped_operation, not_fit_count
def arrange(nodes_to_arrange: List["SceneNode"], def arrange(
nodes_to_arrange: List["SceneNode"],
build_volume: "BuildVolume", build_volume: "BuildVolume",
fixed_nodes: Optional[List["SceneNode"]] = None, fixed_nodes: Optional[List["SceneNode"]] = None,
factor = 10000, factor=10000,
add_new_nodes_in_scene: bool = False) -> bool: 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. 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. :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. are placed.
:param factor: The library that we use is int based. This factor defines how accuracte we want it to be. :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 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 :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() grouped_operation.push()
return not_fit_count == 0 return not_fit_count == 0

View file

@ -75,19 +75,21 @@ class CuraActions(QObject):
center_y = 0 center_y = 0
# Move the object so that it's bottom is on to of the buildplate # 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.addOperation(center_operation)
operation.push() operation.push()
@pyqtSlot(int) @pyqtSlot(int, bool)
def multiplySelection(self, count: int) -> None: def multiplySelection(self, count: int, lock_rotation: bool) -> None:
"""Multiply all objects in the selection """Multiply all objects in the selection
:param count: The number of times to multiply 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 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() job.start()
@pyqtSlot() @pyqtSlot()

View file

@ -1421,8 +1421,8 @@ class CuraApplication(QtApplication):
op.push() op.push()
# Single build plate # Single build plate
@pyqtSlot() @pyqtSlot(bool)
def arrangeAll(self) -> None: def arrangeAll(self, lock_rotation: bool) -> None:
nodes_to_arrange = [] nodes_to_arrange = []
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
locked_nodes = [] locked_nodes = []
@ -1452,17 +1452,18 @@ class CuraApplication(QtApplication):
locked_nodes.append(node) locked_nodes.append(node)
else: else:
nodes_to_arrange.append(node) 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 """Arrange a set of nodes given a set of fixed nodes
:param nodes: nodes that we have to place :param nodes: nodes that we have to place
:param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes :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 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() job.start()
@pyqtSlot() @pyqtSlot()

View file

@ -20,11 +20,12 @@ i18n_catalog = i18nCatalog("cura")
class MultiplyObjectsJob(Job): 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__() super().__init__()
self._objects = objects self._objects = objects
self._count = count self._count: int = count
self._min_offset = min_offset self._min_offset: int = min_offset
self._lock_rotation: bool = lock_rotation
def run(self) -> None: def run(self) -> None:
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime = 0, status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime = 0,
@ -39,7 +40,7 @@ class MultiplyObjectsJob(Job):
root = scene.getRoot() root = scene.getRoot()
processed_nodes = [] # type: List[SceneNode] processed_nodes: List[SceneNode] = []
nodes = [] nodes = []
fixed_nodes = [] fixed_nodes = []
@ -79,8 +80,9 @@ class MultiplyObjectsJob(Job):
group_operation, not_fit_count = createGroupOperationForArrange(nodes, group_operation, not_fit_count = createGroupOperationForArrange(nodes,
Application.getInstance().getBuildVolume(), Application.getInstance().getBuildVolume(),
fixed_nodes, fixed_nodes,
factor = 10000, factor=10000,
add_new_nodes_in_scene = True) add_new_nodes_in_scene=True,
lock_rotation=self._lock_rotation)
found_solution_for_all = not_fit_count == 0 found_solution_for_all = not_fit_count == 0
if nodes_to_add_without_arrange: if nodes_to_add_without_arrange:

View file

@ -41,7 +41,9 @@ Item
property alias deleteAll: deleteAllAction property alias deleteAll: deleteAllAction
property alias reloadAll: reloadAllAction property alias reloadAll: reloadAllAction
property alias arrangeAll: arrangeAllAction property alias arrangeAll: arrangeAllAction
property alias arrangeAllLock: arrangeAllLockAction
property alias arrangeSelection: arrangeSelectionAction property alias arrangeSelection: arrangeSelectionAction
property alias arrangeSelectionLock: arrangeSelectionLockAction
property alias resetAllTranslation: resetAllTranslationAction property alias resetAllTranslation: resetAllTranslationAction
property alias resetAll: resetAllAction property alias resetAll: resetAllAction
@ -412,15 +414,29 @@ Item
{ {
id: arrangeAllAction id: arrangeAllAction
text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models") text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models")
onTriggered: Printer.arrangeAll() onTriggered: Printer.arrangeAll(false)
shortcut: "Ctrl+R" shortcut: "Ctrl+R"
} }
Action
{
id: arrangeAllLockAction
text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models Without Rotation")
onTriggered: Printer.arrangeAll(true)
}
Action Action
{ {
id: arrangeSelectionAction id: arrangeSelectionAction
text: catalog.i18nc("@action:inmenu menubar:edit","Arrange Selection") 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 Action

View file

@ -53,6 +53,7 @@ Cura.Menu
Cura.MenuSeparator {} Cura.MenuSeparator {}
Cura.MenuItem { action: Cura.Actions.selectAll } Cura.MenuItem { action: Cura.Actions.selectAll }
Cura.MenuItem { action: Cura.Actions.arrangeAll } Cura.MenuItem { action: Cura.Actions.arrangeAll }
Cura.MenuItem { action: Cura.Actions.arrangeAllLock }
Cura.MenuItem { action: Cura.Actions.deleteAll } Cura.MenuItem { action: Cura.Actions.deleteAll }
Cura.MenuItem { action: Cura.Actions.reloadAll } Cura.MenuItem { action: Cura.Actions.reloadAll }
Cura.MenuItem { action: Cura.Actions.resetAllTranslation } Cura.MenuItem { action: Cura.Actions.resetAllTranslation }
@ -96,7 +97,7 @@ Cura.Menu
minimumWidth: UM.Theme.getSize("small_popup_dialog").width minimumWidth: UM.Theme.getSize("small_popup_dialog").width
minimumHeight: UM.Theme.getSize("small_popup_dialog").height 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 buttonSpacing: UM.Theme.getSize("thin_margin").width
@ -114,6 +115,10 @@ Cura.Menu
} }
] ]
Column
{
spacing: UM.Theme.getSize("default_margin").height
Row Row
{ {
spacing: UM.Theme.getSize("default_margin").width spacing: UM.Theme.getSize("default_margin").width
@ -137,5 +142,12 @@ Cura.Menu
value: 1 value: 1
} }
} }
UM.CheckBox
{
id: lockRotationField
text: catalog.i18nc("@label", "Lock Rotation")
}
}
} }
} }