mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-21 05:37:50 -06:00
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:
parent
6a5a9e70af
commit
4f9d041ae8
7 changed files with 97 additions and 45 deletions
|
@ -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.")
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -79,15 +79,17 @@ class CuraActions(QObject):
|
||||||
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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 = []
|
||||||
|
@ -80,7 +81,8 @@ class MultiplyObjectsJob(Job):
|
||||||
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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue