PlatformPhysics.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. # Copyright (c) 2022 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from PyQt6.QtCore import QTimer
  4. from UM.Application import Application
  5. from UM.Logger import Logger
  6. from UM.Scene.SceneNode import SceneNode
  7. from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
  8. from UM.Math.Vector import Vector
  9. from UM.Scene.Selection import Selection
  10. from UM.Scene.SceneNodeSettings import SceneNodeSettings
  11. from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
  12. from cura.Operations import PlatformPhysicsOperation
  13. from cura.Scene import ZOffsetDecorator
  14. import random # used for list shuffling
  15. class PlatformPhysics:
  16. def __init__(self, controller, volume):
  17. super().__init__()
  18. self._controller = controller
  19. self._controller.getScene().sceneChanged.connect(self._onSceneChanged)
  20. self._controller.toolOperationStarted.connect(self._onToolOperationStarted)
  21. self._controller.toolOperationStopped.connect(self._onToolOperationStopped)
  22. self._build_volume = volume
  23. self._enabled = True
  24. self._change_timer = QTimer()
  25. self._change_timer.setInterval(100)
  26. self._change_timer.setSingleShot(True)
  27. self._change_timer.timeout.connect(self._onChangeTimerFinished)
  28. self._move_factor = 1.1 # By how much should we multiply overlap to calculate a new spot?
  29. self._max_overlap_checks = 10 # How many times should we try to find a new spot per tick?
  30. self._minimum_gap = 2 # It is a minimum distance (in mm) between two models, applicable for small models
  31. Application.getInstance().getPreferences().addPreference("physics/automatic_push_free", False)
  32. Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", False)
  33. self._app_per_model_drop = Application.getInstance().getPreferences().getValue("physics/automatic_drop_down")
  34. def getAppPerModelDropDown(self):
  35. return self._app_per_model_drop
  36. def setAppPerModelDropDown(self, drop_to_buildplate):
  37. self._app_per_model_drop = drop_to_buildplate
  38. def _onSceneChanged(self, source):
  39. if not source.callDecoration("isSliceable"):
  40. return
  41. self._change_timer.start()
  42. def _onChangeTimerFinished(self):
  43. if not self._enabled:
  44. return
  45. app_instance = Application.getInstance()
  46. app_preferences = app_instance.getPreferences()
  47. app_automatic_drop_down = app_preferences.getValue("physics/automatic_drop_down")
  48. app_automatic_push_free = app_preferences.getValue("physics/automatic_push_free")
  49. root = self._controller.getScene().getRoot()
  50. build_volume = app_instance.getBuildVolume()
  51. build_volume.updateNodeBoundaryCheck()
  52. # Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
  53. # same direction.
  54. transformed_nodes = []
  55. nodes = list(BreadthFirstIterator(root))
  56. # Only check nodes inside build area.
  57. nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea)]
  58. # We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
  59. # By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
  60. random.shuffle(nodes)
  61. for node in nodes:
  62. if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
  63. continue
  64. bbox = node.getBoundingBox()
  65. # Move it downwards if bottom is above platform
  66. move_vector = Vector()
  67. # if per model drop is different then app_automatic_drop, in case of 3mf loading when user changes this setting for that model
  68. if (self._app_per_model_drop != app_automatic_drop_down):
  69. node.setSetting(SceneNodeSettings.AutoDropDown, self._app_per_model_drop)
  70. if node.getSetting(SceneNodeSettings.AutoDropDown, self._app_per_model_drop) and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down
  71. z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
  72. move_vector = move_vector.set(y = -bbox.bottom + z_offset)
  73. # If there is no convex hull for the node, start calculating it and continue.
  74. if not node.getDecorator(ConvexHullDecorator) and not node.callDecoration("isNonPrintingMesh") and node.callDecoration("getLayerData") is None:
  75. node.addDecorator(ConvexHullDecorator())
  76. # only push away objects if this node is a printing mesh
  77. if not node.callDecoration("isNonPrintingMesh") and app_automatic_push_free:
  78. # Do not move locked nodes
  79. if node.getSetting(SceneNodeSettings.LockPosition):
  80. continue
  81. # Check for collisions between convex hulls
  82. for other_node in BreadthFirstIterator(root):
  83. # Ignore root, ourselves and anything that is not a normal SceneNode.
  84. if other_node is root or not issubclass(type(other_node), SceneNode) or other_node is node or other_node.callDecoration("getBuildPlateNumber") != node.callDecoration("getBuildPlateNumber"):
  85. continue
  86. # Ignore collisions of a group with it's own children
  87. if other_node in node.getAllChildren() or node in other_node.getAllChildren():
  88. continue
  89. # Ignore collisions within a group
  90. if other_node.getParent() and node.getParent() and (other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None):
  91. continue
  92. # Ignore nodes that do not have the right properties set.
  93. if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
  94. continue
  95. if other_node in transformed_nodes:
  96. continue # Other node is already moving, wait for next pass.
  97. if other_node.callDecoration("isNonPrintingMesh"):
  98. continue
  99. overlap = (0, 0) # Start loop with no overlap
  100. current_overlap_checks = 0
  101. # Continue to check the overlap until we no longer find one.
  102. while overlap and current_overlap_checks < self._max_overlap_checks:
  103. current_overlap_checks += 1
  104. head_hull = node.callDecoration("getConvexHullHead")
  105. if head_hull: # One at a time intersection.
  106. overlap = head_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_node.callDecoration("getConvexHull"))
  107. if not overlap:
  108. other_head_hull = other_node.callDecoration("getConvexHullHead")
  109. if other_head_hull:
  110. overlap = node.callDecoration("getConvexHull").translate(move_vector.x, move_vector.z).intersectsPolygon(other_head_hull)
  111. if overlap:
  112. # Moving ensured that overlap was still there. Try anew!
  113. move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
  114. z = move_vector.z + overlap[1] * self._move_factor)
  115. else:
  116. # Moving ensured that overlap was still there. Try anew!
  117. move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
  118. z = move_vector.z + overlap[1] * self._move_factor)
  119. else:
  120. own_convex_hull = node.callDecoration("getConvexHull")
  121. other_convex_hull = other_node.callDecoration("getConvexHull")
  122. if own_convex_hull and other_convex_hull:
  123. overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
  124. if overlap: # Moving ensured that overlap was still there. Try anew!
  125. temp_move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
  126. z = move_vector.z + overlap[1] * self._move_factor)
  127. # if the distance between two models less than 2mm then try to find a new factor
  128. if abs(temp_move_vector.x - overlap[0]) < self._minimum_gap and abs(temp_move_vector.y - overlap[1]) < self._minimum_gap:
  129. temp_x_factor = (abs(overlap[0]) + self._minimum_gap) / overlap[0] if overlap[0] != 0 else 0 # find x move_factor, like (3.4 + 2) / 3.4 = 1.58
  130. temp_y_factor = (abs(overlap[1]) + self._minimum_gap) / overlap[1] if overlap[1] != 0 else 0 # find y move_factor
  131. temp_scale_factor = temp_x_factor if abs(temp_x_factor) > abs(temp_y_factor) else temp_y_factor
  132. move_vector = move_vector.set(x = move_vector.x + overlap[0] * temp_scale_factor,
  133. z = move_vector.z + overlap[1] * temp_scale_factor)
  134. else:
  135. move_vector = temp_move_vector
  136. else:
  137. # This can happen in some cases if the object is not yet done with being loaded.
  138. # Simply waiting for the next tick seems to resolve this correctly.
  139. overlap = None
  140. if not Vector.Null.equals(move_vector, epsilon = 1e-5):
  141. transformed_nodes.append(node)
  142. op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
  143. op.push()
  144. # setting this drop to model same as app_automatic_drop_down
  145. self._app_per_model_drop = app_automatic_drop_down
  146. # After moving, we have to evaluate the boundary checks for nodes
  147. build_volume.updateNodeBoundaryCheck()
  148. def _onToolOperationStarted(self, tool):
  149. self._enabled = False
  150. def _onToolOperationStopped(self, tool):
  151. # Selection tool should not trigger an update.
  152. if tool.getPluginId() == "SelectionTool":
  153. return
  154. if tool.getPluginId() == "TranslateTool":
  155. for node in Selection.getAllSelectedObjects():
  156. if node.getBoundingBox() and node.getBoundingBox().bottom < 0:
  157. if not node.getDecorator(ZOffsetDecorator.ZOffsetDecorator):
  158. node.addDecorator(ZOffsetDecorator.ZOffsetDecorator())
  159. node.callDecoration("setZOffset", node.getBoundingBox().bottom)
  160. else:
  161. if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator):
  162. node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
  163. self._enabled = True
  164. self._onChangeTimerFinished()