Arrange.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  2. from UM.Logger import Logger
  3. from UM.Math.Vector import Vector
  4. from cura.Arranging.ShapeArray import ShapeArray
  5. from cura.Scene import ZOffsetDecorator
  6. from collections import namedtuple
  7. import numpy
  8. import copy
  9. ## Return object for bestSpot
  10. LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"])
  11. ## The Arrange classed is used together with ShapeArray. Use it to find
  12. # good locations for objects that you try to put on a build place.
  13. # Different priority schemes can be defined so it alters the behavior while using
  14. # the same logic.
  15. class Arrange:
  16. build_volume = None
  17. def __init__(self, x, y, offset_x, offset_y, scale= 1.0):
  18. self.shape = (y, x)
  19. self._priority = numpy.zeros((x, y), dtype=numpy.int32)
  20. self._priority_unique_values = []
  21. self._occupied = numpy.zeros((x, y), dtype=numpy.int32)
  22. self._scale = scale # convert input coordinates to arrange coordinates
  23. self._offset_x = offset_x
  24. self._offset_y = offset_y
  25. self._last_priority = 0
  26. self._is_empty = True
  27. ## Helper to create an Arranger instance
  28. #
  29. # Either fill in scene_root and create will find all sliceable nodes by itself,
  30. # or use fixed_nodes to provide the nodes yourself.
  31. # \param scene_root Root for finding all scene nodes
  32. # \param fixed_nodes Scene nodes to be placed
  33. @classmethod
  34. def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 220, y = 220):
  35. arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
  36. arranger.centerFirst()
  37. if fixed_nodes is None:
  38. fixed_nodes = []
  39. for node_ in DepthFirstIterator(scene_root):
  40. # Only count sliceable objects
  41. if node_.callDecoration("isSliceable"):
  42. fixed_nodes.append(node_)
  43. # Place all objects fixed nodes
  44. for fixed_node in fixed_nodes:
  45. vertices = fixed_node.callDecoration("getConvexHull")
  46. if not vertices:
  47. continue
  48. points = copy.deepcopy(vertices._points)
  49. shape_arr = ShapeArray.fromPolygon(points, scale = scale)
  50. arranger.place(0, 0, shape_arr)
  51. # If a build volume was set, add the disallowed areas
  52. if Arrange.build_volume:
  53. disallowed_areas = Arrange.build_volume.getDisallowedAreas()
  54. for area in disallowed_areas:
  55. points = copy.deepcopy(area._points)
  56. shape_arr = ShapeArray.fromPolygon(points, scale = scale)
  57. arranger.place(0, 0, shape_arr, update_empty = False)
  58. return arranger
  59. ## Find placement for a node (using offset shape) and place it (using hull shape)
  60. # return the nodes that should be placed
  61. # \param node
  62. # \param offset_shape_arr ShapeArray with offset, used to find location
  63. # \param hull_shape_arr ShapeArray without offset, for placing the shape
  64. def findNodePlacement(self, node, offset_shape_arr, hull_shape_arr, step = 1):
  65. new_node = copy.deepcopy(node)
  66. best_spot = self.bestSpot(
  67. offset_shape_arr, start_prio = self._last_priority, step = step)
  68. x, y = best_spot.x, best_spot.y
  69. # Save the last priority.
  70. self._last_priority = best_spot.priority
  71. # Ensure that the object is above the build platform
  72. new_node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
  73. if new_node.getBoundingBox():
  74. center_y = new_node.getWorldPosition().y - new_node.getBoundingBox().bottom
  75. else:
  76. center_y = 0
  77. if x is not None: # We could find a place
  78. new_node.setPosition(Vector(x, center_y, y))
  79. found_spot = True
  80. self.place(x, y, hull_shape_arr) # place the object in arranger
  81. else:
  82. Logger.log("d", "Could not find spot!"),
  83. found_spot = False
  84. new_node.setPosition(Vector(200, center_y, 100))
  85. return new_node, found_spot
  86. ## Fill priority, center is best. Lower value is better
  87. # This is a strategy for the arranger.
  88. def centerFirst(self):
  89. # Square distance: creates a more round shape
  90. self._priority = numpy.fromfunction(
  91. lambda i, j: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self.shape, dtype=numpy.int32)
  92. self._priority_unique_values = numpy.unique(self._priority)
  93. self._priority_unique_values.sort()
  94. ## Fill priority, back is best. Lower value is better
  95. # This is a strategy for the arranger.
  96. def backFirst(self):
  97. self._priority = numpy.fromfunction(
  98. lambda i, j: 10 * j + abs(self._offset_x - i), self.shape, dtype=numpy.int32)
  99. self._priority_unique_values = numpy.unique(self._priority)
  100. self._priority_unique_values.sort()
  101. ## Return the amount of "penalty points" for polygon, which is the sum of priority
  102. # None if occupied
  103. # \param x x-coordinate to check shape
  104. # \param y y-coordinate
  105. # \param shape_arr the ShapeArray object to place
  106. def checkShape(self, x, y, shape_arr):
  107. x = int(self._scale * x)
  108. y = int(self._scale * y)
  109. offset_x = x + self._offset_x + shape_arr.offset_x
  110. offset_y = y + self._offset_y + shape_arr.offset_y
  111. occupied_slice = self._occupied[
  112. offset_y:offset_y + shape_arr.arr.shape[0],
  113. offset_x:offset_x + shape_arr.arr.shape[1]]
  114. try:
  115. if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
  116. return None
  117. except IndexError: # out of bounds if you try to place an object outside
  118. return None
  119. prio_slice = self._priority[
  120. offset_y:offset_y + shape_arr.arr.shape[0],
  121. offset_x:offset_x + shape_arr.arr.shape[1]]
  122. return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
  123. ## Find "best" spot for ShapeArray
  124. # Return namedtuple with properties x, y, penalty_points, priority
  125. # \param shape_arr ShapeArray
  126. # \param start_prio Start with this priority value (and skip the ones before)
  127. # \param step Slicing value, higher = more skips = faster but less accurate
  128. def bestSpot(self, shape_arr, start_prio = 0, step = 1):
  129. start_idx_list = numpy.where(self._priority_unique_values == start_prio)
  130. if start_idx_list:
  131. start_idx = start_idx_list[0][0]
  132. else:
  133. start_idx = 0
  134. for priority in self._priority_unique_values[start_idx::step]:
  135. tryout_idx = numpy.where(self._priority == priority)
  136. for idx in range(len(tryout_idx[0])):
  137. x = tryout_idx[0][idx]
  138. y = tryout_idx[1][idx]
  139. projected_x = x - self._offset_x
  140. projected_y = y - self._offset_y
  141. # array to "world" coordinates
  142. penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
  143. if penalty_points is not None:
  144. return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
  145. return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority) # No suitable location found :-(
  146. ## Place the object.
  147. # Marks the locations in self._occupied and self._priority
  148. # \param x x-coordinate
  149. # \param y y-coordinate
  150. # \param shape_arr ShapeArray object
  151. # \param update_empty updates the _is_empty, used when adding disallowed areas
  152. def place(self, x, y, shape_arr, update_empty = True):
  153. x = int(self._scale * x)
  154. y = int(self._scale * y)
  155. offset_x = x + self._offset_x + shape_arr.offset_x
  156. offset_y = y + self._offset_y + shape_arr.offset_y
  157. shape_y, shape_x = self._occupied.shape
  158. min_x = min(max(offset_x, 0), shape_x - 1)
  159. min_y = min(max(offset_y, 0), shape_y - 1)
  160. max_x = min(max(offset_x + shape_arr.arr.shape[1], 0), shape_x - 1)
  161. max_y = min(max(offset_y + shape_arr.arr.shape[0], 0), shape_y - 1)
  162. occupied_slice = self._occupied[min_y:max_y, min_x:max_x]
  163. # we use a slice of shape because it can be out of bounds
  164. new_occupied = numpy.where(shape_arr.arr[
  165. min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)
  166. if update_empty and new_occupied:
  167. self._is_empty = False
  168. occupied_slice[new_occupied] = 1
  169. # Set priority to low (= high number), so it won't get picked at trying out.
  170. prio_slice = self._priority[min_y:max_y, min_x:max_x]
  171. prio_slice[numpy.where(shape_arr.arr[
  172. min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999
  173. @property
  174. def isEmpty(self):
  175. return self._is_empty