Arrange.py 8.0 KB

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