Merge branch 'main' into CURA-10542-drop_to_buildplate

This commit is contained in:
Saumya Jain 2024-02-14 14:03:10 +01:00 committed by GitHub
commit be39c1c9ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
165 changed files with 1102 additions and 201 deletions

View file

@ -126,6 +126,7 @@ from .Machines.Models.CompatibleMachineModel import CompatibleMachineModel
from .Machines.Models.MachineListModel import MachineListModel
from .Machines.Models.ActiveIntentQualitiesModel import ActiveIntentQualitiesModel
from .Machines.Models.IntentSelectionModel import IntentSelectionModel
from .PrintOrderManager import PrintOrderManager
from .SingleInstance import SingleInstance
if TYPE_CHECKING:
@ -204,6 +205,7 @@ class CuraApplication(QtApplication):
self._container_manager = None
self._object_manager = None
self._print_order_manager = None
self._extruders_model = None
self._extruders_model_with_optional = None
self._build_plate_model = None
@ -907,6 +909,7 @@ class CuraApplication(QtApplication):
# initialize info objects
self._print_information = PrintInformation.PrintInformation(self)
self._cura_actions = CuraActions.CuraActions(self)
self._print_order_manager = PrintOrderManager(self.getObjectsModel().getNodes)
self.processEvents()
# Initialize setting visibility presets model.
self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self.getPreferences(), parent = self)
@ -989,6 +992,7 @@ class CuraApplication(QtApplication):
t.setEnabledAxis([ToolHandle.XAxis, ToolHandle.YAxis, ToolHandle.ZAxis])
Selection.selectionChanged.connect(self.onSelectionChanged)
self._print_order_manager.printOrderChanged.connect(self._onPrintOrderChanged)
# Set default background color for scene
self.getRenderer().setBackgroundColor(QColor(245, 245, 245))
@ -1262,6 +1266,7 @@ class CuraApplication(QtApplication):
self.processEvents()
engine.rootContext().setContextProperty("Printer", self)
engine.rootContext().setContextProperty("CuraApplication", self)
engine.rootContext().setContextProperty("PrintOrderManager", self._print_order_manager)
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
@ -1757,8 +1762,12 @@ class CuraApplication(QtApplication):
Selection.remove(node)
Selection.add(group_node)
all_nodes = self.getObjectsModel().getNodes()
PrintOrderManager.updatePrintOrdersAfterGroupOperation(all_nodes, group_node, selected_nodes)
@pyqtSlot()
def ungroupSelected(self) -> None:
all_nodes = self.getObjectsModel().getNodes()
selected_objects = Selection.getAllSelectedObjects().copy()
for node in selected_objects:
if node.callDecoration("isGroup"):
@ -1766,21 +1775,30 @@ class CuraApplication(QtApplication):
group_parent = node.getParent()
children = node.getChildren().copy()
for child in children:
# Ungroup only 1 level deep
if child.getParent() != node:
continue
# Ungroup only 1 level deep
children_to_ungroup = list(filter(lambda child: child.getParent() == node, children))
for child in children_to_ungroup:
# 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)
PrintOrderManager.updatePrintOrdersAfterUngroupOperation(all_nodes, node, children_to_ungroup)
op.push()
# Note: The group removes itself from the scene once all its children have left it,
# see GroupDecorator._onChildrenChanged
def _onPrintOrderChanged(self) -> None:
# update object list
scene = self.getController().getScene()
scene.sceneChanged.emit(scene.getRoot())
# reset if already was sliced
Application.getInstance().getBackend().needsSlicing()
Application.getInstance().getBackend().tickle()
def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
if self._is_headless:
return None

88
cura/HitChecker.py Normal file
View file

@ -0,0 +1,88 @@
from typing import List, Dict
from cura.Scene.CuraSceneNode import CuraSceneNode
class HitChecker:
"""Checks if nodes can be printed without causing any collisions and interference"""
def __init__(self, nodes: List[CuraSceneNode]) -> None:
self._hit_map = self._buildHitMap(nodes)
def anyTwoNodesBlockEachOther(self, nodes: List[CuraSceneNode]) -> bool:
"""Returns True if any 2 nodes block each other"""
for a in nodes:
for b in nodes:
if self._hit_map[a][b] and self._hit_map[b][a]:
return True
return False
def canPrintBefore(self, node: CuraSceneNode, other_nodes: List[CuraSceneNode]) -> bool:
"""Returns True if node doesn't block other_nodes and can be printed before them"""
no_hits = all(not self._hit_map[node][other_node] for other_node in other_nodes)
return no_hits
def canPrintAfter(self, node: CuraSceneNode, other_nodes: List[CuraSceneNode]) -> bool:
"""Returns True if node doesn't hit other nodes and can be printed after them"""
no_hits = all(not self._hit_map[other_node][node] for other_node in other_nodes)
return no_hits
def calculateScore(self, a: CuraSceneNode, b: CuraSceneNode) -> int:
"""Calculate score simply sums the number of other objects it 'blocks'
:param a: node
:param b: node
:return: sum of the number of other objects
"""
score_a = sum(self._hit_map[a].values())
score_b = sum(self._hit_map[b].values())
return score_a - score_b
def canPrintNodesInProvidedOrder(self, ordered_nodes: List[CuraSceneNode]) -> bool:
"""Returns True If nodes don't have any hits in provided order"""
for node_index, node in enumerate(ordered_nodes):
nodes_before = ordered_nodes[:node_index - 1] if node_index - 1 >= 0 else []
nodes_after = ordered_nodes[node_index + 1:] if node_index + 1 < len(ordered_nodes) else []
if not self.canPrintBefore(node, nodes_after) or not self.canPrintAfter(node, nodes_before):
return False
return True
@staticmethod
def _buildHitMap(nodes: List[CuraSceneNode]) -> Dict[CuraSceneNode, CuraSceneNode]:
"""Pre-computes all hits between all objects
:nodes: nodes that need to be checked for collisions
:return: dictionary where hit_map[node1][node2] is False if there node1 can be printed before node2
"""
hit_map = {j: {i: HitChecker._checkHit(j, i) for i in nodes} for j in nodes}
return hit_map
@staticmethod
def _checkHit(a: CuraSceneNode, b: CuraSceneNode) -> bool:
"""Checks if a can be printed before b
:param a: node
:param b: node
:return: False if a can be printed before b
"""
if a == b:
return False
a_hit_hull = a.callDecoration("getConvexHullBoundary")
b_hit_hull = b.callDecoration("getConvexHullHeadFull")
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
if overlap:
return True
# Adhesion areas must never overlap, regardless of printing order
# This would cause over-extrusion
a_hit_hull = a.callDecoration("getAdhesionArea")
b_hit_hull = b.callDecoration("getAdhesionArea")
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
if overlap:
return True
else:
return False

View file

@ -7,6 +7,11 @@ from UM.Scene.Iterator import Iterator
from UM.Scene.SceneNode import SceneNode
from functools import cmp_to_key
from cura.HitChecker import HitChecker
from cura.PrintOrderManager import PrintOrderManager
from cura.Scene.CuraSceneNode import CuraSceneNode
class OneAtATimeIterator(Iterator.Iterator):
"""Iterator that returns a list of nodes in the order that they need to be printed
@ -16,8 +21,6 @@ class OneAtATimeIterator(Iterator.Iterator):
def __init__(self, scene_node) -> None:
super().__init__(scene_node) # Call super to make multiple inheritance work.
self._hit_map = [[]] # type: List[List[bool]] # For each node, which other nodes this hits. A grid of booleans on which nodes hit which.
self._original_node_list = [] # type: List[SceneNode] # The nodes that need to be checked for collisions.
def _fillStack(self) -> None:
"""Fills the ``_node_stack`` with a list of scene nodes that need to be printed in order. """
@ -38,104 +41,50 @@ class OneAtATimeIterator(Iterator.Iterator):
self._node_stack = node_list[:]
return
# Copy the list
self._original_node_list = node_list[:]
hit_checker = HitChecker(node_list)
# Initialise the hit map (pre-compute all hits between all objects)
self._hit_map = [[self._checkHit(i, j) for i in node_list] for j in node_list]
if PrintOrderManager.isUserDefinedPrintOrderEnabled():
self._node_stack = self._getNodesOrderedByUser(hit_checker, node_list)
else:
self._node_stack = self._getNodesOrderedAutomatically(hit_checker, node_list)
# Check if we have to files that block each other. If this is the case, there is no solution!
for a in range(0, len(node_list)):
for b in range(0, len(node_list)):
if a != b and self._hit_map[a][b] and self._hit_map[b][a]:
return
# update print orders so that user can try to arrange the nodes automatically first
# and if result is not satisfactory he/she can switch to manual mode and change it
for index, node in enumerate(self._node_stack):
node.printOrder = index + 1
@staticmethod
def _getNodesOrderedByUser(hit_checker: HitChecker, node_list: List[CuraSceneNode]) -> List[CuraSceneNode]:
nodes_ordered_by_user = sorted(node_list, key=lambda n: n.printOrder)
if hit_checker.canPrintNodesInProvidedOrder(nodes_ordered_by_user):
return nodes_ordered_by_user
return [] # No solution
@staticmethod
def _getNodesOrderedAutomatically(hit_checker: HitChecker, node_list: List[CuraSceneNode]) -> List[CuraSceneNode]:
# Check if we have two files that block each other. If this is the case, there is no solution!
if hit_checker.anyTwoNodesBlockEachOther(node_list):
return [] # No solution
# Sort the original list so that items that block the most other objects are at the beginning.
# This does not decrease the worst case running time, but should improve it in most cases.
sorted(node_list, key = cmp_to_key(self._calculateScore))
node_list = sorted(node_list, key = cmp_to_key(hit_checker.calculateScore))
todo_node_list = [_ObjectOrder([], node_list)]
while len(todo_node_list) > 0:
current = todo_node_list.pop()
for node in current.todo:
# Check if the object can be placed with what we have and still allows for a solution in the future
if not self._checkHitMultiple(node, current.order) and not self._checkBlockMultiple(node, current.todo):
if hit_checker.canPrintAfter(node, current.order) and hit_checker.canPrintBefore(node, current.todo):
# We found a possible result. Create new todo & order list.
new_todo_list = current.todo[:]
new_todo_list.remove(node)
new_order = current.order[:] + [node]
if len(new_todo_list) == 0:
# We have no more nodes to check, so quit looking.
self._node_stack = new_order
return
return new_order # Solution found!
todo_node_list.append(_ObjectOrder(new_order, new_todo_list))
self._node_stack = [] #No result found!
# Check if first object can be printed before the provided list (using the hit map)
def _checkHitMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool:
node_index = self._original_node_list.index(node)
for other_node in other_nodes:
other_node_index = self._original_node_list.index(other_node)
if self._hit_map[node_index][other_node_index]:
return True
return False
def _checkBlockMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool:
"""Check for a node whether it hits any of the other nodes.
:param node: The node to check whether it collides with the other nodes.
:param other_nodes: The nodes to check for collisions.
:return: returns collision between nodes
"""
node_index = self._original_node_list.index(node)
for other_node in other_nodes:
other_node_index = self._original_node_list.index(other_node)
if self._hit_map[other_node_index][node_index] and node_index != other_node_index:
return True
return False
def _calculateScore(self, a: SceneNode, b: SceneNode) -> int:
"""Calculate score simply sums the number of other objects it 'blocks'
:param a: node
:param b: node
:return: sum of the number of other objects
"""
score_a = sum(self._hit_map[self._original_node_list.index(a)])
score_b = sum(self._hit_map[self._original_node_list.index(b)])
return score_a - score_b
def _checkHit(self, a: SceneNode, b: SceneNode) -> bool:
"""Checks if a can be printed before b
:param a: node
:param b: node
:return: true if a can be printed before b
"""
if a == b:
return False
a_hit_hull = a.callDecoration("getConvexHullBoundary")
b_hit_hull = b.callDecoration("getConvexHullHeadFull")
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
if overlap:
return True
# Adhesion areas must never overlap, regardless of printing order
# This would cause over-extrusion
a_hit_hull = a.callDecoration("getAdhesionArea")
b_hit_hull = b.callDecoration("getAdhesionArea")
overlap = a_hit_hull.intersectsPolygon(b_hit_hull)
if overlap:
return True
else:
return False
return [] # No result found!
class _ObjectOrder:

171
cura/PrintOrderManager.py Normal file
View file

@ -0,0 +1,171 @@
from typing import List, Callable, Optional, Any
from PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject, pyqtSlot
from UM.Application import Application
from UM.Scene.Selection import Selection
from cura.Scene.CuraSceneNode import CuraSceneNode
class PrintOrderManager(QObject):
"""Allows to order the object list to set the print sequence manually"""
def __init__(self, get_nodes: Callable[[], List[CuraSceneNode]]) -> None:
super().__init__()
self._get_nodes = get_nodes
self._configureEvents()
_settingsChanged = pyqtSignal()
_uiActionsOutdated = pyqtSignal()
printOrderChanged = pyqtSignal()
@pyqtSlot()
def swapSelectedAndPreviousNodes(self) -> None:
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
self._swapPrintOrders(selected_node, previous_node)
@pyqtSlot()
def swapSelectedAndNextNodes(self) -> None:
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
self._swapPrintOrders(selected_node, next_node)
@pyqtProperty(str, notify=_uiActionsOutdated)
def previousNodeName(self) -> str:
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
return self._getNodeName(previous_node)
@pyqtProperty(str, notify=_uiActionsOutdated)
def nextNodeName(self) -> str:
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
return self._getNodeName(next_node)
@pyqtProperty(bool, notify=_uiActionsOutdated)
def shouldEnablePrintBeforeAction(self) -> bool:
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
can_swap_with_previous_node = selected_node is not None and previous_node is not None
return can_swap_with_previous_node
@pyqtProperty(bool, notify=_uiActionsOutdated)
def shouldEnablePrintAfterAction(self) -> bool:
selected_node, previous_node, next_node = self._getSelectedAndNeighborNodes()
can_swap_with_next_node = selected_node is not None and next_node is not None
return can_swap_with_next_node
@pyqtProperty(bool, notify=_settingsChanged)
def shouldShowEditPrintOrderActions(self) -> bool:
return PrintOrderManager.isUserDefinedPrintOrderEnabled()
@staticmethod
def isUserDefinedPrintOrderEnabled() -> bool:
stack = Application.getInstance().getGlobalContainerStack()
is_enabled = stack and \
stack.getProperty("print_sequence", "value") == "one_at_a_time" and \
stack.getProperty("user_defined_print_order_enabled", "value")
return bool(is_enabled)
@staticmethod
def initializePrintOrders(nodes: List[CuraSceneNode]) -> None:
"""Just created (loaded from file) nodes have print order 0.
This method initializes print orders with max value to put nodes at the end of object list"""
max_print_order = max(map(lambda n: n.printOrder, nodes), default=0)
for node in nodes:
if node.printOrder == 0:
max_print_order += 1
node.printOrder = max_print_order
@staticmethod
def updatePrintOrdersAfterGroupOperation(
all_nodes: List[CuraSceneNode],
group_node: CuraSceneNode,
grouped_nodes: List[CuraSceneNode]
) -> None:
group_node.printOrder = min(map(lambda n: n.printOrder, grouped_nodes))
all_nodes.append(group_node)
for node in grouped_nodes:
all_nodes.remove(node)
# reassign print orders so there won't be gaps like 1 2 5 6 7
sorted_nodes = sorted(all_nodes, key=lambda n: n.printOrder)
for i, node in enumerate(sorted_nodes):
node.printOrder = i + 1
@staticmethod
def updatePrintOrdersAfterUngroupOperation(
all_nodes: List[CuraSceneNode],
group_node: CuraSceneNode,
ungrouped_nodes: List[CuraSceneNode]
) -> None:
all_nodes.remove(group_node)
nodes_to_update_print_order = filter(lambda n: n.printOrder > group_node.printOrder, all_nodes)
for node in nodes_to_update_print_order:
node.printOrder += len(ungrouped_nodes) - 1
for i, child in enumerate(ungrouped_nodes):
child.printOrder = group_node.printOrder + i
all_nodes.append(child)
def _swapPrintOrders(self, node1: CuraSceneNode, node2: CuraSceneNode) -> None:
if node1 and node2:
node1.printOrder, node2.printOrder = node2.printOrder, node1.printOrder # swap print orders
self.printOrderChanged.emit() # update object list first
self._uiActionsOutdated.emit() # then update UI actions
def _getSelectedAndNeighborNodes(self
) -> (Optional[CuraSceneNode], Optional[CuraSceneNode], Optional[CuraSceneNode]):
nodes = self._get_nodes()
ordered_nodes = sorted(nodes, key=lambda n: n.printOrder)
selected_node = PrintOrderManager._getSingleSelectedNode()
if selected_node and selected_node in ordered_nodes:
selected_node_index = ordered_nodes.index(selected_node)
else:
selected_node_index = None
if selected_node_index is not None and selected_node_index - 1 >= 0:
previous_node = ordered_nodes[selected_node_index - 1]
else:
previous_node = None
if selected_node_index is not None and selected_node_index + 1 < len(ordered_nodes):
next_node = ordered_nodes[selected_node_index + 1]
else:
next_node = None
return selected_node, previous_node, next_node
@staticmethod
def _getNodeName(node: CuraSceneNode, max_length: int = 30) -> str:
node_name = node.getName() if node else ""
truncated_node_name = node_name[:max_length]
return truncated_node_name
@staticmethod
def _getSingleSelectedNode() -> Optional[CuraSceneNode]:
if len(Selection.getAllSelectedObjects()) == 1:
selected_node = Selection.getSelectedObject(0)
return selected_node
return None
def _configureEvents(self) -> None:
Selection.selectionChanged.connect(self._onSelectionChanged)
self._global_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
def _onGlobalStackChanged(self) -> None:
if self._global_stack:
self._global_stack.propertyChanged.disconnect(self._onSettingsChanged)
self._global_stack.containersChanged.disconnect(self._onSettingsChanged)
self._global_stack = Application.getInstance().getGlobalContainerStack()
if self._global_stack:
self._global_stack.propertyChanged.connect(self._onSettingsChanged)
self._global_stack.containersChanged.connect(self._onSettingsChanged)
def _onSettingsChanged(self, *args: Any) -> None:
self._settingsChanged.emit()
def _onSelectionChanged(self) -> None:
self._uiActionsOutdated.emit()

View file

@ -25,10 +25,19 @@ class CuraSceneNode(SceneNode):
if not no_setting_override:
self.addDecorator(SettingOverrideDecorator()) # Now we always have a getActiveExtruderPosition, unless explicitly disabled
self._outside_buildarea = False
self._print_order = 0
def setOutsideBuildArea(self, new_value: bool) -> None:
self._outside_buildarea = new_value
@property
def printOrder(self):
return self._print_order
@printOrder.setter
def printOrder(self, new_value):
self._print_order = new_value
def isOutsideBuildArea(self) -> bool:
return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0
@ -157,3 +166,6 @@ class CuraSceneNode(SceneNode):
def transformChanged(self) -> None:
self._transformChanged()
def __repr__(self) -> str:
return "{print_order}. {name}".format(print_order = self._print_order, name = self.getName())

View file

@ -14,6 +14,9 @@ from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.i18n import i18nCatalog
from cura.PrintOrderManager import PrintOrderManager
from cura.Scene.CuraSceneNode import CuraSceneNode
catalog = i18nCatalog("cura")
@ -76,6 +79,9 @@ class ObjectsModel(ListModel):
self._build_plate_number = nr
self._update()
def getNodes(self) -> List[CuraSceneNode]:
return list(map(lambda n: n["node"], self.items))
def _updateSceneDelayed(self, source) -> None:
if not isinstance(source, Camera):
self._update_timer.start()
@ -175,6 +181,10 @@ class ObjectsModel(ListModel):
all_nodes = self._renameNodes(name_to_node_info_dict)
user_defined_print_order_enabled = PrintOrderManager.isUserDefinedPrintOrderEnabled()
if user_defined_print_order_enabled:
PrintOrderManager.initializePrintOrders(all_nodes)
for node in all_nodes:
if hasattr(node, "isOutsideBuildArea"):
is_outside_build_area = node.isOutsideBuildArea() # type: ignore
@ -223,8 +233,13 @@ class ObjectsModel(ListModel):
# for anti overhang meshes and groups the extruder nr is irrelevant
extruder_number = -1
if not user_defined_print_order_enabled:
name = node.getName()
else:
name = "{print_order}. {name}".format(print_order = node.printOrder, name = node.getName())
nodes.append({
"name": node.getName(),
"name": name,
"selected": Selection.isSelected(node),
"outside_build_area": is_outside_build_area,
"buildplate_number": node_build_plate_number,
@ -234,5 +249,5 @@ class ObjectsModel(ListModel):
"node": node
})
nodes = sorted(nodes, key=lambda n: n["name"])
nodes = sorted(nodes, key=lambda n: n["name"] if not user_defined_print_order_enabled else n["node"].printOrder)
self.setItems(nodes)