ConvexHullDecorator.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
  2. from UM.Application import Application
  3. from UM.Math.Polygon import Polygon
  4. from . import ConvexHullNode
  5. import numpy
  6. ## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node.
  7. # If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed.
  8. class ConvexHullDecorator(SceneNodeDecorator):
  9. def __init__(self):
  10. super().__init__()
  11. self._convex_hull_node = None
  12. self._init2DConvexHullCache()
  13. self._global_stack = None
  14. Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
  15. Application.getInstance().getController().toolOperationStarted.connect(self._onChanged)
  16. Application.getInstance().getController().toolOperationStopped.connect(self._onChanged)
  17. self._onGlobalStackChanged()
  18. def setNode(self, node):
  19. previous_node = self._node
  20. if previous_node is not None and node is not previous_node:
  21. previous_node.transformationChanged.connect(self._onChanged)
  22. previous_node.parentChanged.connect(self._onChanged)
  23. super().setNode(node)
  24. self._node.transformationChanged.connect(self._onChanged)
  25. self._node.parentChanged.connect(self._onChanged)
  26. self._onChanged()
  27. ## Force that a new (empty) object is created upon copy.
  28. def __deepcopy__(self, memo):
  29. return ConvexHullDecorator()
  30. ## Get the unmodified 2D projected convex hull of the node
  31. def getConvexHull(self):
  32. if self._node is None:
  33. return None
  34. hull = self._compute2DConvexHull()
  35. if self._global_stack and self._node:
  36. if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
  37. hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32)))
  38. return hull
  39. ## Get the convex hull of the node with the full head size
  40. def getConvexHullHeadFull(self):
  41. if self._node is None:
  42. return None
  43. return self._compute2DConvexHeadFull()
  44. ## Get convex hull of the object + head size
  45. # In case of printing all at once this is the same as the convex hull.
  46. # For one at the time this is area with intersection of mirrored head
  47. def getConvexHullHead(self):
  48. if self._node is None:
  49. return None
  50. if self._global_stack:
  51. if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
  52. return self._compute2DConvexHeadMin()
  53. return None
  54. ## Get convex hull of the node
  55. # In case of printing all at once this is the same as the convex hull.
  56. # For one at the time this is the area without the head.
  57. def getConvexHullBoundary(self):
  58. if self._node is None:
  59. return None
  60. if self._global_stack:
  61. if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
  62. # Printing one at a time and it's not an object in a group
  63. return self._compute2DConvexHull()
  64. return None
  65. def recomputeConvexHull(self):
  66. controller = Application.getInstance().getController()
  67. root = controller.getScene().getRoot()
  68. if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node):
  69. if self._convex_hull_node:
  70. self._convex_hull_node.setParent(None)
  71. self._convex_hull_node = None
  72. return
  73. convex_hull = self.getConvexHull()
  74. if self._convex_hull_node:
  75. if self._convex_hull_node.getHull() == convex_hull:
  76. return
  77. self._convex_hull_node.setParent(None)
  78. hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, root)
  79. self._convex_hull_node = hull_node
  80. def _onSettingValueChanged(self, key, property_name):
  81. if key == "print_sequence" and property_name == "value":
  82. self._onChanged()
  83. def _init2DConvexHullCache(self):
  84. # Cache for the group code path in _compute2DConvexHull()
  85. self._2d_convex_hull_group_child_polygon = None
  86. self._2d_convex_hull_group_result = None
  87. # Cache for the mesh code path in _compute2DConvexHull()
  88. self._2d_convex_hull_mesh = None
  89. self._2d_convex_hull_mesh_world_transform = None
  90. self._2d_convex_hull_mesh_result = None
  91. def _compute2DConvexHull(self):
  92. if self._node.callDecoration("isGroup"):
  93. points = numpy.zeros((0, 2), dtype=numpy.int32)
  94. for child in self._node.getChildren():
  95. child_hull = child.callDecoration("_compute2DConvexHull")
  96. if child_hull:
  97. points = numpy.append(points, child_hull.getPoints(), axis = 0)
  98. if points.size < 3:
  99. return None
  100. child_polygon = Polygon(points)
  101. # Check the cache
  102. if child_polygon == self._2d_convex_hull_group_child_polygon:
  103. return self._2d_convex_hull_group_result
  104. # First, calculate the normal convex hull around the points
  105. convex_hull = child_polygon.getConvexHull()
  106. # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull.
  107. # This is done because of rounding errors.
  108. rounded_hull = self._roundHull(convex_hull)
  109. # Store the result in the cache
  110. self._2d_convex_hull_group_child_polygon = child_polygon
  111. self._2d_convex_hull_group_result = rounded_hull
  112. return rounded_hull
  113. else:
  114. rounded_hull = None
  115. mesh = None
  116. world_transform = None
  117. if self._node.getMeshData():
  118. mesh = self._node.getMeshData()
  119. world_transform = self._node.getWorldTransformation()
  120. # Check the cache
  121. if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:
  122. return self._2d_convex_hull_mesh_result
  123. vertex_data = mesh.getConvexHullTransformedVertices(world_transform)
  124. # Don't use data below 0.
  125. # TODO; We need a better check for this as this gives poor results for meshes with long edges.
  126. vertex_data = vertex_data[vertex_data[:,1] >= 0]
  127. if len(vertex_data) >= 4:
  128. # Round the vertex data to 1/10th of a mm, then remove all duplicate vertices
  129. # This is done to greatly speed up further convex hull calculations as the convex hull
  130. # becomes much less complex when dealing with highly detailed models.
  131. vertex_data = numpy.round(vertex_data, 1)
  132. vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D.
  133. # Grab the set of unique points.
  134. #
  135. # This basically finds the unique rows in the array by treating them as opaque groups of bytes
  136. # which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch.
  137. # See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
  138. vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
  139. numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
  140. _, idx = numpy.unique(vertex_byte_view, return_index=True)
  141. vertex_data = vertex_data[idx] # Select the unique rows by index.
  142. hull = Polygon(vertex_data)
  143. if len(vertex_data) >= 4:
  144. # First, calculate the normal convex hull around the points
  145. convex_hull = hull.getConvexHull()
  146. # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull.
  147. # This is done because of rounding errors.
  148. rounded_hull = convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32)))
  149. # Store the result in the cache
  150. self._2d_convex_hull_mesh = mesh
  151. self._2d_convex_hull_mesh_world_transform = world_transform
  152. self._2d_convex_hull_mesh_result = rounded_hull
  153. return rounded_hull
  154. def _getHeadAndFans(self):
  155. return Polygon(numpy.array(self._global_stack.getProperty("machine_head_with_fans_polygon", "value"), numpy.float32))
  156. def _compute2DConvexHeadFull(self):
  157. return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans())
  158. def _compute2DConvexHeadMin(self):
  159. headAndFans = self._getHeadAndFans()
  160. mirrored = headAndFans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically.
  161. head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
  162. # Min head hull is used for the push free
  163. min_head_hull = self._compute2DConvexHull().getMinkowskiHull(head_and_fans)
  164. return min_head_hull
  165. def _roundHull(self, convex_hull):
  166. return convex_hull.getMinkowskiHull(Polygon(numpy.array([[-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5], [0.5, -0.5]], numpy.float32)))
  167. def _onChanged(self, *args):
  168. self.recomputeConvexHull()
  169. def _onGlobalStackChanged(self):
  170. if self._global_stack:
  171. self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged)
  172. self._global_stack.containersChanged.disconnect(self._onChanged)
  173. self._global_stack = Application.getInstance().getGlobalContainerStack()
  174. if self._global_stack:
  175. self._global_stack.propertyChanged.connect(self._onSettingValueChanged)
  176. self._global_stack.containersChanged.connect(self._onChanged)
  177. self._onChanged()
  178. ## Returns true if node is a descendent or the same as the root node.
  179. def __isDescendant(self, root, node):
  180. if node is None:
  181. return False
  182. if root is node:
  183. return True
  184. return self.__isDescendant(root, node.getParent())