Arrange.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  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. shape_arr = ShapeArray.fromPolygon(points, scale = scale)
  67. arranger.place(0, 0, shape_arr)
  68. # If a build volume was set, add the disallowed areas
  69. if Arrange.build_volume:
  70. disallowed_areas = Arrange.build_volume.getDisallowedAreasNoBrim()
  71. for area in disallowed_areas:
  72. points = copy.deepcopy(area._points)
  73. shape_arr = ShapeArray.fromPolygon(points, scale = scale)
  74. arranger.place(0, 0, shape_arr, update_empty = False)
  75. return arranger
  76. def resetLastPriority(self):
  77. """This resets the optimization for finding location based on size"""
  78. self._last_priority = 0
  79. def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1) -> bool:
  80. """Find placement for a node (using offset shape) and place it (using hull shape)
  81. :param node: The node to be placed
  82. :param offset_shape_arr: shape array with offset, for placing the shape
  83. :param hull_shape_arr: shape array without offset, used to find location
  84. :param step: default = 1
  85. :return: the nodes that should be placed
  86. """
  87. best_spot = self.bestSpot(
  88. hull_shape_arr, start_prio = self._last_priority, step = step)
  89. x, y = best_spot.x, best_spot.y
  90. # Save the last priority.
  91. self._last_priority = best_spot.priority
  92. # Ensure that the object is above the build platform
  93. node.removeDecorator(ZOffsetDecorator.ZOffsetDecorator)
  94. bbox = node.getBoundingBox()
  95. if bbox:
  96. center_y = node.getWorldPosition().y - bbox.bottom
  97. else:
  98. center_y = 0
  99. if x is not None: # We could find a place
  100. node.setPosition(Vector(x, center_y, y))
  101. found_spot = True
  102. self.place(x, y, offset_shape_arr) # place the object in arranger
  103. else:
  104. Logger.log("d", "Could not find spot!")
  105. found_spot = False
  106. node.setPosition(Vector(200, center_y, 100))
  107. return found_spot
  108. def centerFirst(self):
  109. """Fill priority, center is best. Lower value is better. """
  110. # Square distance: creates a more round shape
  111. self._priority = numpy.fromfunction(
  112. lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32)
  113. self._priority_unique_values = numpy.unique(self._priority)
  114. self._priority_unique_values.sort()
  115. def backFirst(self):
  116. """Fill priority, back is best. Lower value is better """
  117. self._priority = numpy.fromfunction(
  118. lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32)
  119. self._priority_unique_values = numpy.unique(self._priority)
  120. self._priority_unique_values.sort()
  121. def checkShape(self, x, y, shape_arr) -> Optional[numpy.ndarray]:
  122. """Return the amount of "penalty points" for polygon, which is the sum of priority
  123. :param x: x-coordinate to check shape
  124. :param y: y-coordinate to check shape
  125. :param shape_arr: the shape array object to place
  126. :return: None if occupied
  127. """
  128. x = int(self._scale * x)
  129. y = int(self._scale * y)
  130. offset_x = x + self._offset_x + shape_arr.offset_x
  131. offset_y = y + self._offset_y + shape_arr.offset_y
  132. if offset_x < 0 or offset_y < 0:
  133. return None # out of bounds in self._occupied
  134. occupied_x_max = offset_x + shape_arr.arr.shape[1]
  135. occupied_y_max = offset_y + shape_arr.arr.shape[0]
  136. if occupied_x_max > self._occupied.shape[1] + 1 or occupied_y_max > self._occupied.shape[0] + 1:
  137. return None # out of bounds in self._occupied
  138. occupied_slice = self._occupied[
  139. offset_y:occupied_y_max,
  140. offset_x:occupied_x_max]
  141. try:
  142. if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
  143. return None
  144. except IndexError: # out of bounds if you try to place an object outside
  145. return None
  146. prio_slice = self._priority[
  147. offset_y:offset_y + shape_arr.arr.shape[0],
  148. offset_x:offset_x + shape_arr.arr.shape[1]]
  149. return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
  150. def bestSpot(self, shape_arr, start_prio = 0, step = 1) -> LocationSuggestion:
  151. """Find "best" spot for ShapeArray
  152. :param shape_arr: shape array
  153. :param start_prio: Start with this priority value (and skip the ones before)
  154. :param step: Slicing value, higher = more skips = faster but less accurate
  155. :return: namedtuple with properties x, y, penalty_points, priority.
  156. """
  157. start_idx_list = numpy.where(self._priority_unique_values == start_prio)
  158. if start_idx_list:
  159. try:
  160. start_idx = start_idx_list[0][0]
  161. except IndexError:
  162. start_idx = 0
  163. else:
  164. start_idx = 0
  165. priority = 0
  166. for priority in self._priority_unique_values[start_idx::step]:
  167. tryout_idx = numpy.where(self._priority == priority)
  168. for idx in range(len(tryout_idx[0])):
  169. x = tryout_idx[1][idx]
  170. y = tryout_idx[0][idx]
  171. projected_x = int((x - self._offset_x) / self._scale)
  172. projected_y = int((y - self._offset_y) / self._scale)
  173. penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
  174. if penalty_points is not None:
  175. return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
  176. return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority) # No suitable location found :-(
  177. def place(self, x, y, shape_arr, update_empty = True):
  178. """Place the object.
  179. Marks the locations in self._occupied and self._priority
  180. :param x:
  181. :param y:
  182. :param shape_arr:
  183. :param update_empty: updates the _is_empty, used when adding disallowed areas
  184. """
  185. x = int(self._scale * x)
  186. y = int(self._scale * y)
  187. offset_x = x + self._offset_x + shape_arr.offset_x
  188. offset_y = y + self._offset_y + shape_arr.offset_y
  189. shape_y, shape_x = self._occupied.shape
  190. min_x = min(max(offset_x, 0), shape_x - 1)
  191. min_y = min(max(offset_y, 0), shape_y - 1)
  192. max_x = min(max(offset_x + shape_arr.arr.shape[1], 0), shape_x - 1)
  193. max_y = min(max(offset_y + shape_arr.arr.shape[0], 0), shape_y - 1)
  194. occupied_slice = self._occupied[min_y:max_y, min_x:max_x]
  195. # we use a slice of shape because it can be out of bounds
  196. new_occupied = numpy.where(shape_arr.arr[
  197. min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)
  198. if update_empty and new_occupied:
  199. self._is_empty = False
  200. occupied_slice[new_occupied] = 1
  201. # Set priority to low (= high number), so it won't get picked at trying out.
  202. prio_slice = self._priority[min_y:max_y, min_x:max_x]
  203. prio_slice[new_occupied] = 999
  204. @property
  205. def isEmpty(self):
  206. return self._is_empty