Skip to content

Commit

Permalink
Merge pull request #17893 from alexandr-vladimirov/Issue-4835-allow-t…
Browse files Browse the repository at this point in the history
…o-set-print-sequence-manually

Allow to set print sequence manually
  • Loading branch information
saumyaj3 authored Feb 9, 2024
2 parents 578057f + a7292c7 commit 579b0fe
Show file tree
Hide file tree
Showing 53 changed files with 1,022 additions and 89 deletions.
8 changes: 8 additions & 0 deletions conandata.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ pycharm_targets:
module_name: Cura
name: pytest in TestGCodeListDecorator.py
script_name: tests/TestGCodeListDecorator.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestHitChecker.py
script_name: tests/TestHitChecker.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestIntentManager.py
Expand Down Expand Up @@ -187,6 +191,10 @@ pycharm_targets:
module_name: Cura
name: pytest in TestPrintInformation.py
script_name: tests/TestPrintInformation.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestPrintOrderManager.py
script_name: tests/TestPrintOrderManager.py
- jinja_path: .run_templates/pycharm_cura_test.run.xml.jinja
module_name: Cura
name: pytest in TestProfileRequirements.py
Expand Down
26 changes: 22 additions & 4 deletions cura/CuraApplication.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
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:
Expand Down Expand Up @@ -204,6 +205,7 @@ def __init__(self, *args, **kwargs):
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
Expand Down Expand Up @@ -907,6 +909,7 @@ def run(self):
# 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)
Expand Down Expand Up @@ -989,6 +992,7 @@ def runWithGUI(self):
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))
Expand Down Expand Up @@ -1262,6 +1266,7 @@ def registerObjects(self, engine):
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)
Expand Down Expand Up @@ -1757,30 +1762,43 @@ def groupSelected(self) -> None:
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"):
op = GroupedOperation()

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
Expand Down
88 changes: 88 additions & 0 deletions cura/HitChecker.py
Original file line number Diff line number Diff line change
@@ -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
115 changes: 32 additions & 83 deletions cura/OneAtATimeIterator.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
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
Expand All @@ -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. """
Expand All @@ -38,104 +41,50 @@ def _fillStack(self) -> None:
self._node_stack = node_list[:]
return

# Copy the list
self._original_node_list = 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]
hit_checker = HitChecker(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
if PrintOrderManager.isUserDefinedPrintOrderEnabled():
self._node_stack = self._getNodesOrderedByUser(hit_checker, node_list)
else:
self._node_stack = self._getNodesOrderedAutomatically(hit_checker, node_list)

# 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:
Expand Down
Loading

0 comments on commit 579b0fe

Please sign in to comment.