ConvexHullDecorator.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  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. self._raft_thickness = 0.0
  15. # For raft thickness, DRY
  16. self._build_volume = Application.getInstance().getBuildVolume()
  17. self._build_volume.raftThicknessChanged.connect(self._onChanged)
  18. Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
  19. Application.getInstance().getController().toolOperationStarted.connect(self._onChanged)
  20. Application.getInstance().getController().toolOperationStopped.connect(self._onChanged)
  21. self._onGlobalStackChanged()
  22. def setNode(self, node):
  23. previous_node = self._node
  24. # Disconnect from previous node signals
  25. if previous_node is not None and node is not previous_node:
  26. previous_node.transformationChanged.disconnect(self._onChanged)
  27. previous_node.parentChanged.disconnect(self._onChanged)
  28. super().setNode(node)
  29. self._node.transformationChanged.connect(self._onChanged)
  30. self._node.parentChanged.connect(self._onChanged)
  31. self._onChanged()
  32. ## Force that a new (empty) object is created upon copy.
  33. def __deepcopy__(self, memo):
  34. return ConvexHullDecorator()
  35. ## Get the unmodified 2D projected convex hull of the node
  36. def getConvexHull(self):
  37. if self._node is None:
  38. return None
  39. hull = self._compute2DConvexHull()
  40. if self._global_stack and self._node:
  41. if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
  42. hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32)))
  43. hull = self._add2DAdhesionMargin(hull)
  44. return hull
  45. ## Get the convex hull of the node with the full head size
  46. def getConvexHullHeadFull(self):
  47. if self._node is None:
  48. return None
  49. return self._compute2DConvexHeadFull()
  50. ## Get convex hull of the object + head size
  51. # In case of printing all at once this is the same as the convex hull.
  52. # For one at the time this is area with intersection of mirrored head
  53. def getConvexHullHead(self):
  54. if self._node is None:
  55. return None
  56. if self._global_stack:
  57. if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
  58. head_with_fans = self._compute2DConvexHeadMin()
  59. head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans)
  60. return head_with_fans_with_adhesion_margin
  61. return None
  62. ## Get convex hull of the node
  63. # In case of printing all at once this is the same as the convex hull.
  64. # For one at the time this is the area without the head.
  65. def getConvexHullBoundary(self):
  66. if self._node is None:
  67. return None
  68. if self._global_stack:
  69. if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self._node.getParent().callDecoration("isGroup"):
  70. # Printing one at a time and it's not an object in a group
  71. return self._compute2DConvexHull()
  72. return None
  73. def recomputeConvexHull(self):
  74. controller = Application.getInstance().getController()
  75. root = controller.getScene().getRoot()
  76. if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node):
  77. if self._convex_hull_node:
  78. self._convex_hull_node.setParent(None)
  79. self._convex_hull_node = None
  80. return
  81. convex_hull = self.getConvexHull()
  82. if self._convex_hull_node:
  83. self._convex_hull_node.setParent(None)
  84. hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, self._raft_thickness, root)
  85. self._convex_hull_node = hull_node
  86. def _onSettingValueChanged(self, key, property_name):
  87. if key in self._affected_settings and property_name == "value":
  88. self._onChanged()
  89. def _init2DConvexHullCache(self):
  90. # Cache for the group code path in _compute2DConvexHull()
  91. self._2d_convex_hull_group_child_polygon = None
  92. self._2d_convex_hull_group_result = None
  93. # Cache for the mesh code path in _compute2DConvexHull()
  94. self._2d_convex_hull_mesh = None
  95. self._2d_convex_hull_mesh_world_transform = None
  96. self._2d_convex_hull_mesh_result = None
  97. def _compute2DConvexHull(self):
  98. if self._node.callDecoration("isGroup"):
  99. points = numpy.zeros((0, 2), dtype=numpy.int32)
  100. for child in self._node.getChildren():
  101. child_hull = child.callDecoration("_compute2DConvexHull")
  102. if child_hull:
  103. points = numpy.append(points, child_hull.getPoints(), axis = 0)
  104. if points.size < 3:
  105. return None
  106. child_polygon = Polygon(points)
  107. # Check the cache
  108. if child_polygon == self._2d_convex_hull_group_child_polygon:
  109. return self._2d_convex_hull_group_result
  110. # First, calculate the normal convex hull around the points
  111. convex_hull = child_polygon.getConvexHull()
  112. # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull.
  113. # This is done because of rounding errors.
  114. rounded_hull = self._roundHull(convex_hull)
  115. # Store the result in the cache
  116. self._2d_convex_hull_group_child_polygon = child_polygon
  117. self._2d_convex_hull_group_result = rounded_hull
  118. return rounded_hull
  119. else:
  120. rounded_hull = None
  121. mesh = None
  122. world_transform = None
  123. if self._node.getMeshData():
  124. mesh = self._node.getMeshData()
  125. world_transform = self._node.getWorldTransformation()
  126. # Check the cache
  127. if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:
  128. return self._2d_convex_hull_mesh_result
  129. vertex_data = mesh.getConvexHullTransformedVertices(world_transform)
  130. # Don't use data below 0.
  131. # TODO; We need a better check for this as this gives poor results for meshes with long edges.
  132. # Do not throw away vertices: the convex hull may be too small and objects can collide.
  133. # vertex_data = vertex_data[vertex_data[:,1] >= -0.01]
  134. if len(vertex_data) >= 4:
  135. # Round the vertex data to 1/10th of a mm, then remove all duplicate vertices
  136. # This is done to greatly speed up further convex hull calculations as the convex hull
  137. # becomes much less complex when dealing with highly detailed models.
  138. vertex_data = numpy.round(vertex_data, 1)
  139. vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D.
  140. # Grab the set of unique points.
  141. #
  142. # This basically finds the unique rows in the array by treating them as opaque groups of bytes
  143. # which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch.
  144. # See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
  145. vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
  146. numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
  147. _, idx = numpy.unique(vertex_byte_view, return_index=True)
  148. vertex_data = vertex_data[idx] # Select the unique rows by index.
  149. hull = Polygon(vertex_data)
  150. if len(vertex_data) >= 4:
  151. # First, calculate the normal convex hull around the points
  152. convex_hull = hull.getConvexHull()
  153. # Then, do a Minkowski hull with a simple 1x1 quad to outset and round the normal convex hull.
  154. # This is done because of rounding errors.
  155. 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)))
  156. # Store the result in the cache
  157. self._2d_convex_hull_mesh = mesh
  158. self._2d_convex_hull_mesh_world_transform = world_transform
  159. self._2d_convex_hull_mesh_result = rounded_hull
  160. return rounded_hull
  161. def _getHeadAndFans(self):
  162. return Polygon(numpy.array(self._global_stack.getProperty("machine_head_with_fans_polygon", "value"), numpy.float32))
  163. def _compute2DConvexHeadFull(self):
  164. return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans())
  165. def _compute2DConvexHeadMin(self):
  166. headAndFans = self._getHeadAndFans()
  167. mirrored = headAndFans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically.
  168. head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
  169. # Min head hull is used for the push free
  170. min_head_hull = self._compute2DConvexHull().getMinkowskiHull(head_and_fans)
  171. return min_head_hull
  172. ## Compensate given 2D polygon with adhesion margin
  173. # \return 2D polygon with added margin
  174. def _add2DAdhesionMargin(self, poly):
  175. # Compensate for raft/skirt/brim
  176. # Add extra margin depending on adhesion type
  177. adhesion_type = self._global_stack.getProperty("adhesion_type", "value")
  178. extra_margin = 0
  179. machine_head_coords = numpy.array(
  180. self._global_stack.getProperty("machine_head_with_fans_polygon", "value"),
  181. numpy.float32)
  182. if adhesion_type == "raft":
  183. extra_margin = max(0, self._global_stack.getProperty("raft_margin", "value"))
  184. elif adhesion_type == "brim":
  185. extra_margin = max(0, self._global_stack.getProperty("brim_line_count", "value") * self._global_stack.getProperty("skirt_brim_line_width", "value"))
  186. elif adhesion_type == "skirt":
  187. extra_margin = max(
  188. 0, self._global_stack.getProperty("skirt_gap", "value") +
  189. self._global_stack.getProperty("skirt_line_count", "value") * self._global_stack.getProperty("skirt_brim_line_width", "value"))
  190. # adjust head_and_fans with extra margin
  191. if extra_margin > 0:
  192. # In Cura 2.2+, there is a function to create this circle-like polygon.
  193. extra_margin_polygon = Polygon(numpy.array([
  194. [-extra_margin, 0],
  195. [-extra_margin * 0.707, extra_margin * 0.707],
  196. [0, extra_margin],
  197. [extra_margin * 0.707, extra_margin * 0.707],
  198. [extra_margin, 0],
  199. [extra_margin * 0.707, -extra_margin * 0.707],
  200. [0, -extra_margin],
  201. [-extra_margin * 0.707, -extra_margin * 0.707]
  202. ], numpy.float32))
  203. poly = poly.getMinkowskiHull(extra_margin_polygon)
  204. return poly
  205. def _roundHull(self, convex_hull):
  206. 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)))
  207. def _onChanged(self, *args):
  208. self._raft_thickness = self._build_volume.getRaftThickness()
  209. self.recomputeConvexHull()
  210. def _onGlobalStackChanged(self):
  211. if self._global_stack:
  212. self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged)
  213. self._global_stack.containersChanged.disconnect(self._onChanged)
  214. self._global_stack = Application.getInstance().getGlobalContainerStack()
  215. if self._global_stack:
  216. self._global_stack.propertyChanged.connect(self._onSettingValueChanged)
  217. self._global_stack.containersChanged.connect(self._onChanged)
  218. self._onChanged()
  219. ## Returns true if node is a descendent or the same as the root node.
  220. def __isDescendant(self, root, node):
  221. if node is None:
  222. return False
  223. if root is node:
  224. return True
  225. return self.__isDescendant(root, node.getParent())
  226. _affected_settings = [
  227. "adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers",
  228. "raft_surface_thickness", "raft_airgap", "raft_margin", "print_sequence",
  229. "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"]