PlatformPhysics.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  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. class PlatformPhysics:
  14. def __init__(self, controller, volume):
  15. super().__init__()
  16. self._controller = controller
  17. self._controller.getScene().sceneChanged.connect(self._onSceneChanged)
  18. self._controller.toolOperationStarted.connect(self._onToolOperationStarted)
  19. self._controller.toolOperationStopped.connect(self._onToolOperationStopped)
  20. self._build_volume = volume
  21. self._enabled = True
  22. self._change_timer = QTimer()
  23. self._change_timer.setInterval(100)
  24. self._change_timer.setSingleShot(True)
  25. self._change_timer.timeout.connect(self._onChangeTimerFinished)
  26. Preferences.getInstance().addPreference("physics/automatic_push_free", True)
  27. def _onSceneChanged(self, source):
  28. self._change_timer.start()
  29. def _onChangeTimerFinished(self):
  30. if not self._enabled:
  31. return
  32. root = self._controller.getScene().getRoot()
  33. for node in BreadthFirstIterator(root):
  34. if node is root or type(node) is not SceneNode or node.getBoundingBox() is None:
  35. continue
  36. bbox = node.getBoundingBox()
  37. # Ignore intersections with the bottom
  38. build_volume_bounding_box = self._build_volume.getBoundingBox()
  39. if build_volume_bounding_box:
  40. # It's over 9000!
  41. build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001)
  42. else:
  43. # No bounding box. This is triggered when running Cura from command line with a model for the first time
  44. # In that situation there is a model, but no machine (and therefore no build volume.
  45. return
  46. node._outside_buildarea = False
  47. # Mark the node as outside the build volume if the bounding box test fails.
  48. if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
  49. node._outside_buildarea = True
  50. # Move it downwards if bottom is above platform
  51. move_vector = Vector()
  52. if not (node.getParent() and node.getParent().callDecoration("isGroup")): #If an object is grouped, don't move it down
  53. z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
  54. move_vector = move_vector.set(y=-bbox.bottom + z_offset)
  55. # If there is no convex hull for the node, start calculating it and continue.
  56. if not node.getDecorator(ConvexHullDecorator):
  57. node.addDecorator(ConvexHullDecorator())
  58. node.callDecoration("recomputeConvexHull")
  59. if Preferences.getInstance().getValue("physics/automatic_push_free"):
  60. # Check for collisions between convex hulls
  61. for other_node in BreadthFirstIterator(root):
  62. # Ignore root, ourselves and anything that is not a normal SceneNode.
  63. if other_node is root or type(other_node) is not SceneNode or other_node is node:
  64. continue
  65. # Ignore collisions of a group with it's own children
  66. if other_node in node.getAllChildren() or node in other_node.getAllChildren():
  67. continue
  68. # Ignore collisions within a group
  69. if other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None:
  70. continue
  71. # Ignore nodes that do not have the right properties set.
  72. if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox():
  73. continue
  74. # Get the overlap distance for both convex hulls. If this returns None, there is no intersection.
  75. head_hull = node.callDecoration("getConvexHullHead")
  76. if head_hull:
  77. overlap = head_hull.intersectsPolygon(other_node.callDecoration("getConvexHull"))
  78. if not overlap:
  79. other_head_hull = other_node.callDecoration("getConvexHullHead")
  80. if other_head_hull:
  81. overlap = node.callDecoration("getConvexHull").intersectsPolygon(other_head_hull)
  82. else:
  83. own_convex_hull = node.callDecoration("getConvexHull")
  84. other_convex_hull = other_node.callDecoration("getConvexHull")
  85. if own_convex_hull and other_convex_hull:
  86. overlap = own_convex_hull.intersectsPolygon(other_convex_hull)
  87. else:
  88. # This can happen in some cases if the object is not yet done with being loaded.
  89. # Simply waiting for the next tick seems to resolve this correctly.
  90. overlap = None
  91. if overlap is None:
  92. continue
  93. move_vector = move_vector.set(x=overlap[0] * 1.1, z=overlap[1] * 1.1)
  94. convex_hull = node.callDecoration("getConvexHull")
  95. if convex_hull:
  96. if not convex_hull.isValid():
  97. return
  98. # Check for collisions between disallowed areas and the object
  99. for area in self._build_volume.getDisallowedAreas():
  100. overlap = convex_hull.intersectsPolygon(area)
  101. if overlap is None:
  102. continue
  103. node._outside_buildarea = True
  104. if not Vector.Null.equals(move_vector, epsilon=1e-5):
  105. op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
  106. op.push()
  107. def _onToolOperationStarted(self, tool):
  108. self._enabled = False
  109. def _onToolOperationStopped(self, tool):
  110. if tool.getPluginId() == "TranslateTool":
  111. for node in Selection.getAllSelectedObjects():
  112. if node.getBoundingBox().bottom < 0:
  113. if not node.getDecorator(ZOffsetDecorator.ZOffsetDecorator):
  114. node.addDecorator(ZOffsetDecorator.ZOffsetDecorator())
  115. node.callDecoration("setZOffset", node.getBoundingBox().bottom)
  116. else:
  117. if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator):
  118. node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
  119. self._enabled = True
  120. self._onChangeTimerFinished()