PlatformPhysics.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. # Copyright (c) 2015 Ultimaker B.V.
  2. # Cura is released under the terms of the AGPLv3 or higher.
  3. from PyQt5.QtCore import QTimer
  4. from UM.Scene.SceneNode import SceneNode
  5. from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
  6. from UM.Math.Vector import Vector
  7. from UM.Math.AxisAlignedBox import AxisAlignedBox
  8. from UM.Scene.Selection import Selection
  9. from UM.Preferences import Preferences
  10. from cura.ConvexHullDecorator import ConvexHullDecorator
  11. from . import PlatformPhysicsOperation
  12. from . import ZOffsetDecorator
  13. import random # used for list shuffling
  14. class PlatformPhysics:
  15. def __init__(self, controller, volume):
  16. super().__init__()
  17. self._controller = controller
  18. self._controller.getScene().sceneChanged.connect(self._onSceneChanged)
  19. self._controller.toolOperationStarted.connect(self._onToolOperationStarted)
  20. self._controller.toolOperationStopped.connect(self._onToolOperationStopped)
  21. self._build_volume = volume
  22. self._enabled = True
  23. self._change_timer = QTimer()
  24. self._change_timer.setInterval(100)
  25. self._change_timer.setSingleShot(True)
  26. self._change_timer.timeout.connect(self._onChangeTimerFinished)
  27. self._move_factor = 1.1 # By how much should we multiply overlap to calculate a new spot?
  28. self._max_overlap_checks = 10 # How many times should we try to find a new spot per tick?
  29. Preferences.getInstance().addPreference("physics/automatic_push_free", True)
  30. Preferences.getInstance().addPreference("physics/automatic_drop_down", True)
  31. def _onSceneChanged(self, source):
  32. self._change_timer.start()
  33. def _onChangeTimerFinished(self):
  34. if not self._enabled:
  35. return
  36. root = self._controller.getScene().getRoot()
  37. # Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
  38. # same direction.
  39. transformed_nodes = []
  40. group_nodes = []
  41. # We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
  42. # By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
  43. nodes = list(BreadthFirstIterator(root))
  44. random.shuffle(nodes)
  45. for node in nodes:
  46. if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
  47. continue
  48. bbox = node.getBoundingBox()
  49. # Ignore intersections with the bottom
  50. build_volume_bounding_box = self._build_volume.getBoundingBox()
  51. if build_volume_bounding_box:
  52. # It's over 9000!
  53. build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
  54. else:
  55. # No bounding box. This is triggered when running Cura from command line with a model for the first time
  56. # In that situation there is a model, but no machine (and therefore no build volume.
  57. return
  58. node._outside_buildarea = False
  59. # Mark the node as outside the build volume if the bounding box test fails.
  60. if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
  61. node._outside_buildarea = True
  62. if node.callDecoration("isGroup"):
  63. group_nodes.append(node) # Keep list of affected group_nodes
  64. # Move it downwards if bottom is above platform
  65. move_vector = Vector()
  66. if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")) and node.isEnabled(): #If an object is grouped, don't move it down
  67. z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
  68. move_vector = move_vector.set(y=-bbox.bottom + z_offset)
  69. # If there is no convex hull for the node, start calculating it and continue.
  70. if not node.getDecorator(ConvexHullDecorator):
  71. node.addDecorator(ConvexHullDecorator())
  72. if Preferences.getInstance().getValue("physics/automatic_push_free"):
  73. # Check for collisions between convex hulls
  74. for other_node in BreadthFirstIterator(root):
  75. # Ignore root, ourselves and anything that is not a normal SceneNode.
  76. if other_node is root or type(other_node) is not SceneNode or other_node is node:
  77. continue
  78. # Ignore collisions of a group with it's own children
  79. if other_node in node.getAllChildren() or node in other_node.getAllChildren():
  80. continue
  81. # Ignore collisions within a group
  82. if other_node.getParent() and node.getParent() and (other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None):
  83. continue
  84. # Ignore nodes that do not have the right properties set.
  85. if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
  86. continue
  87. if other_node in transformed_nodes:
  88. continue # Other node is already moving, wait for next pass.
  89. overlap = (0, 0) # Start loop with no overlap
  90. current_overlap_checks = 0
  91. # Continue to check the overlap until we no longer find one.
  92. while overlap and current_overlap_checks < self._max_overlap_checks:
  93. current_overlap_checks += 1
  94. head_hull = node.callDecoration("getConvexHullHead")
  95. if head_hull: # One at a time intersection.
  96. overlap = head_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_node.callDecoration("getConvexHull"))
  97. if not overlap:
  98. other_head_hull = other_node.callDecoration("getConvexHullHead")
  99. if other_head_hull:
  100. overlap = node.callDecoration("getConvexHull").translate(move_vector.x, move_vector.z).intersectsPolygon(other_head_hull)
  101. if overlap:
  102. # Moving ensured that overlap was still there. Try anew!
  103. move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor,
  104. z=move_vector.z + overlap[1] * self._move_factor)
  105. else:
  106. # Moving ensured that overlap was still there. Try anew!
  107. move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor,
  108. z=move_vector.z + overlap[1] * self._move_factor)
  109. else:
  110. own_convex_hull = node.callDecoration("getConvexHull")
  111. other_convex_hull = other_node.callDecoration("getConvexHull")
  112. if own_convex_hull and other_convex_hull:
  113. overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
  114. if overlap: # Moving ensured that overlap was still there. Try anew!
  115. move_vector = move_vector.set(x=move_vector.x + overlap[0] * self._move_factor,
  116. z=move_vector.z + overlap[1] * self._move_factor)
  117. else:
  118. # This can happen in some cases if the object is not yet done with being loaded.
  119. # Simply waiting for the next tick seems to resolve this correctly.
  120. overlap = None
  121. convex_hull = node.callDecoration("getConvexHull")
  122. if convex_hull:
  123. if not convex_hull.isValid():
  124. return
  125. # Check for collisions between disallowed areas and the object
  126. for area in self._build_volume.getDisallowedAreas():
  127. overlap = convex_hull.intersectsPolygon(area)
  128. if overlap is None:
  129. continue
  130. node._outside_buildarea = True
  131. if not Vector.Null.equals(move_vector, epsilon=1e-5):
  132. transformed_nodes.append(node)
  133. op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
  134. op.push()
  135. # Group nodes should override the _outside_buildarea property of their children.
  136. for group_node in group_nodes:
  137. for child_node in group_node.getAllChildren():
  138. child_node._outside_buildarea = group_node._outside_buildarea
  139. def _onToolOperationStarted(self, tool):
  140. self._enabled = False
  141. def _onToolOperationStopped(self, tool):
  142. # Selection tool should not trigger an update.
  143. if tool.getPluginId() == "SelectionTool":
  144. return
  145. if tool.getPluginId() == "TranslateTool":
  146. for node in Selection.getAllSelectedObjects():
  147. if node.getBoundingBox().bottom < 0:
  148. if not node.getDecorator(ZOffsetDecorator.ZOffsetDecorator):
  149. node.addDecorator(ZOffsetDecorator.ZOffsetDecorator())
  150. node.callDecoration("setZOffset", node.getBoundingBox().bottom)
  151. else:
  152. if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator):
  153. node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
  154. self._enabled = True
  155. self._onChangeTimerFinished()