Arrange.py 11 KB

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