Arrange.py 11 KB

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