Arrange.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
  2. from UM.Logger import Logger
  3. from UM.Math.Vector import Vector
  4. from cura.ShapeArray import ShapeArray
  5. from cura 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. points = copy.deepcopy(vertices._points)
  47. shape_arr = ShapeArray.fromPolygon(points, scale = scale)
  48. arranger.place(0, 0, shape_arr)
  49. # If a build volume was set, add the disallowed areas
  50. if Arrange.build_volume:
  51. disallowed_areas = Arrange.build_volume.getDisallowedAreas()
  52. for area in disallowed_areas:
  53. points = copy.deepcopy(area._points)
  54. shape_arr = ShapeArray.fromPolygon(points, scale = scale)
  55. arranger.place(0, 0, shape_arr, update_empty = False)
  56. return arranger
  57. ## Find placement for a node (using offset shape) and place it (using hull shape)
  58. # return the nodes that should be placed
  59. # \param node
  60. # \param offset_shape_arr ShapeArray with offset, used to find location
  61. # \param hull_shape_arr ShapeArray without offset, for placing the shape
  62. def findNodePlacement(self, node, offset_shape_arr, hull_shape_arr, step = 1):
  63. new_node = copy.deepcopy(node)
  64. best_spot = self.bestSpot(
  65. offset_shape_arr, start_prio = self._last_priority, step = step)
  66. x, y = best_spot.x, best_spot.y
  67. # Save the last priority.
  68. self._last_priority = best_spot.priority
  69. # Ensure that the object is above the build platform
  70. new_node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
  71. if new_node.getBoundingBox():
  72. center_y = new_node.getWorldPosition().y - new_node.getBoundingBox().bottom
  73. else:
  74. center_y = 0
  75. if x is not None: # We could find a place
  76. new_node.setPosition(Vector(x, center_y, y))
  77. found_spot = True
  78. self.place(x, y, hull_shape_arr) # place the object in arranger
  79. else:
  80. Logger.log("d", "Could not find spot!"),
  81. found_spot = False
  82. new_node.setPosition(Vector(200, center_y, 100))
  83. return new_node, found_spot
  84. ## Fill priority, center is best. Lower value is better
  85. # This is a strategy for the arranger.
  86. def centerFirst(self):
  87. # Square distance: creates a more round shape
  88. self._priority = numpy.fromfunction(
  89. lambda i, j: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self.shape, dtype=numpy.int32)
  90. self._priority_unique_values = numpy.unique(self._priority)
  91. self._priority_unique_values.sort()
  92. ## Fill priority, back is best. Lower value is better
  93. # This is a strategy for the arranger.
  94. def backFirst(self):
  95. self._priority = numpy.fromfunction(
  96. lambda i, j: 10 * j + abs(self._offset_x - i), self.shape, dtype=numpy.int32)
  97. self._priority_unique_values = numpy.unique(self._priority)
  98. self._priority_unique_values.sort()
  99. ## Return the amount of "penalty points" for polygon, which is the sum of priority
  100. # None if occupied
  101. # \param x x-coordinate to check shape
  102. # \param y y-coordinate
  103. # \param shape_arr the ShapeArray object to place
  104. def checkShape(self, x, y, shape_arr):
  105. x = int(self._scale * x)
  106. y = int(self._scale * y)
  107. offset_x = x + self._offset_x + shape_arr.offset_x
  108. offset_y = y + self._offset_y + shape_arr.offset_y
  109. occupied_slice = self._occupied[
  110. offset_y:offset_y + shape_arr.arr.shape[0],
  111. offset_x:offset_x + shape_arr.arr.shape[1]]
  112. try:
  113. if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
  114. return None
  115. except IndexError: # out of bounds if you try to place an object outside
  116. return None
  117. prio_slice = self._priority[
  118. offset_y:offset_y + shape_arr.arr.shape[0],
  119. offset_x:offset_x + shape_arr.arr.shape[1]]
  120. return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
  121. ## Find "best" spot for ShapeArray
  122. # Return namedtuple with properties x, y, penalty_points, priority
  123. # \param shape_arr ShapeArray
  124. # \param start_prio Start with this priority value (and skip the ones before)
  125. # \param step Slicing value, higher = more skips = faster but less accurate
  126. def bestSpot(self, shape_arr, start_prio = 0, step = 1):
  127. start_idx_list = numpy.where(self._priority_unique_values == start_prio)
  128. if start_idx_list:
  129. start_idx = start_idx_list[0][0]
  130. else:
  131. start_idx = 0
  132. for priority in self._priority_unique_values[start_idx::step]:
  133. tryout_idx = numpy.where(self._priority == priority)
  134. for idx in range(len(tryout_idx[0])):
  135. x = tryout_idx[0][idx]
  136. y = tryout_idx[1][idx]
  137. projected_x = x - self._offset_x
  138. projected_y = y - self._offset_y
  139. # array to "world" coordinates
  140. penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
  141. if penalty_points is not None:
  142. return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
  143. return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority) # No suitable location found :-(
  144. ## Place the object.
  145. # Marks the locations in self._occupied and self._priority
  146. # \param x x-coordinate
  147. # \param y y-coordinate
  148. # \param shape_arr ShapeArray object
  149. def place(self, x, y, shape_arr, update_empty = True):
  150. x = int(self._scale * x)
  151. y = int(self._scale * y)
  152. offset_x = x + self._offset_x + shape_arr.offset_x
  153. offset_y = y + self._offset_y + shape_arr.offset_y
  154. shape_y, shape_x = self._occupied.shape
  155. min_x = min(max(offset_x, 0), shape_x - 1)
  156. min_y = min(max(offset_y, 0), shape_y - 1)
  157. max_x = min(max(offset_x + shape_arr.arr.shape[1], 0), shape_x - 1)
  158. max_y = min(max(offset_y + shape_arr.arr.shape[0], 0), shape_y - 1)
  159. occupied_slice = self._occupied[min_y:max_y, min_x:max_x]
  160. # we use a slice of shape because it can be out of bounds
  161. new_occupied = numpy.where(shape_arr.arr[
  162. min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)
  163. if update_empty and new_occupied:
  164. self._is_empty = False
  165. occupied_slice[new_occupied] = 1
  166. # Set priority to low (= high number), so it won't get picked at trying out.
  167. prio_slice = self._priority[min_y:max_y, min_x:max_x]
  168. prio_slice[numpy.where(shape_arr.arr[
  169. min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999
  170. @property
  171. def isEmpty(self):
  172. return self._is_empty