Arrange.py 10 KB

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