Arrange.py 8.3 KB

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