GridArrange.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import math
  2. from typing import List, TYPE_CHECKING, Tuple, Set, Union
  3. if TYPE_CHECKING:
  4. from UM.Scene.SceneNode import SceneNode
  5. from cura.BuildVolume import BuildVolume
  6. from UM.Application import Application
  7. from UM.Math.AxisAlignedBox import AxisAlignedBox
  8. from UM.Math.Polygon import Polygon
  9. from UM.Math.Vector import Vector
  10. from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
  11. from UM.Operations.GroupedOperation import GroupedOperation
  12. from UM.Operations.TranslateOperation import TranslateOperation
  13. from cura.Arranging.Arranger import Arranger
  14. class GridArrange(Arranger):
  15. def __init__(self, nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: List["SceneNode"] = None):
  16. if fixed_nodes is None:
  17. fixed_nodes = []
  18. self._nodes_to_arrange = nodes_to_arrange
  19. self._build_volume = build_volume
  20. self._build_volume_bounding_box = build_volume.getBoundingBox()
  21. self._fixed_nodes = fixed_nodes
  22. self._margin_x: float = 1
  23. self._margin_y: float = 1
  24. self._grid_width = 0
  25. self._grid_height = 0
  26. for node in self._nodes_to_arrange:
  27. bounding_box = node.getBoundingBox()
  28. self._grid_width = max(self._grid_width, bounding_box.width)
  29. self._grid_height = max(self._grid_height, bounding_box.depth)
  30. self._grid_width += self._margin_x
  31. self._grid_height += self._margin_y
  32. # Round up the grid size to the nearest cm, this assures that new objects will
  33. # be placed on integer offsets from each other
  34. grid_precision = 10 # 1cm
  35. rounded_grid_width = math.ceil(self._grid_width / grid_precision) * grid_precision
  36. rounded_grid_height = math.ceil(self._grid_height / grid_precision) * grid_precision
  37. # The space added by the "grid precision rounding up" of the grid size
  38. self._grid_round_margin_x = rounded_grid_width - self._grid_width
  39. self._grid_round_margin_y = rounded_grid_height - self._grid_height
  40. self._grid_width = rounded_grid_width
  41. self._grid_height = rounded_grid_height
  42. self._offset_x = 0
  43. self._offset_y = 0
  44. self._findOptimalGridOffset()
  45. coord_initial_leftover_x = self._build_volume_bounding_box.right + 2 * self._grid_width
  46. coord_initial_leftover_y = (self._build_volume_bounding_box.back + self._build_volume_bounding_box.front) * 0.5
  47. self._initial_leftover_grid_x, self._initial_leftover_grid_y = self._coordSpaceToGridSpace(
  48. coord_initial_leftover_x, coord_initial_leftover_y)
  49. self._initial_leftover_grid_x = math.floor(self._initial_leftover_grid_x)
  50. self._initial_leftover_grid_y = math.floor(self._initial_leftover_grid_y)
  51. # Find grid indexes that intersect with fixed objects
  52. self._fixed_nodes_grid_ids = set()
  53. for node in self._fixed_nodes:
  54. self._fixed_nodes_grid_ids = self._fixed_nodes_grid_ids.union(
  55. self._intersectingGridIdxInclusive(node.getBoundingBox()))
  56. # grid indexes that are in disallowed area
  57. for polygon in self._build_volume.getDisallowedAreas():
  58. self._fixed_nodes_grid_ids = self._fixed_nodes_grid_ids.union(self._intersectingGridIdxInclusive(polygon))
  59. self._build_plate_grid_ids = self._intersectingGridIdxExclusive(self._build_volume_bounding_box)
  60. # Filter out the corner grid squares if the build plate shape is elliptic
  61. if self._build_volume.getShape() == "elliptic":
  62. self._build_plate_grid_ids = set(
  63. filter(lambda grid_id: self._checkGridUnderDiscSpace(grid_id[0], grid_id[1]),
  64. self._build_plate_grid_ids))
  65. self._allowed_grid_idx = self._build_plate_grid_ids.difference(self._fixed_nodes_grid_ids)
  66. def createGroupOperationForArrange(self, *, add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]:
  67. # Find the sequence in which items are placed
  68. coord_build_plate_center_x = self._build_volume_bounding_box.width * 0.5 + self._build_volume_bounding_box.left
  69. coord_build_plate_center_y = self._build_volume_bounding_box.depth * 0.5 + self._build_volume_bounding_box.back
  70. grid_build_plate_center_x, grid_build_plate_center_y = self._coordSpaceToGridSpace(coord_build_plate_center_x,
  71. coord_build_plate_center_y)
  72. sequence: List[Tuple[int, int]] = list(self._allowed_grid_idx)
  73. sequence.sort(key=lambda grid_id: (grid_build_plate_center_x - grid_id[0]) ** 2 + (
  74. grid_build_plate_center_y - grid_id[1]) ** 2)
  75. scene_root = Application.getInstance().getController().getScene().getRoot()
  76. grouped_operation = GroupedOperation()
  77. for grid_id, node in zip(sequence, self._nodes_to_arrange):
  78. if add_new_nodes_in_scene:
  79. grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
  80. grid_x, grid_y = grid_id
  81. operation = self._moveNodeOnGrid(node, grid_x, grid_y)
  82. grouped_operation.addOperation(operation)
  83. leftover_nodes = self._nodes_to_arrange[len(sequence):]
  84. left_over_grid_y = self._initial_leftover_grid_y
  85. for node in leftover_nodes:
  86. if add_new_nodes_in_scene:
  87. grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
  88. # find the first next grid position that isn't occupied by a fixed node
  89. while (self._initial_leftover_grid_x, left_over_grid_y) in self._fixed_nodes_grid_ids:
  90. left_over_grid_y = left_over_grid_y - 1
  91. operation = self._moveNodeOnGrid(node, self._initial_leftover_grid_x, left_over_grid_y)
  92. grouped_operation.addOperation(operation)
  93. left_over_grid_y = left_over_grid_y - 1
  94. return grouped_operation, len(leftover_nodes)
  95. def _findOptimalGridOffset(self):
  96. if len(self._fixed_nodes) == 0:
  97. self._offset_x = 0
  98. self._offset_y = 0
  99. return
  100. if len(self._fixed_nodes) == 1:
  101. center_grid_x = 0.5 * self._grid_width + self._build_volume_bounding_box.left
  102. center_grid_y = 0.5 * self._grid_height + self._build_volume_bounding_box.back
  103. bounding_box = self._fixed_nodes[0].getBoundingBox()
  104. center_node_x = (bounding_box.left + bounding_box.right) * 0.5
  105. center_node_y = (bounding_box.back + bounding_box.front) * 0.5
  106. self._offset_x = center_node_x - center_grid_x
  107. self._offset_y = center_node_y - center_grid_y
  108. return
  109. # If there are multiple fixed nodes, an optimal solution is not always possible
  110. # We will try to find an offset that minimizes the number of grid intersections
  111. # with fixed nodes. The algorithm below achieves this by utilizing a scanline
  112. # algorithm. In this algorithm each axis is solved separately as offsetting
  113. # is completely independent in each axis. The comments explaining the algorithm
  114. # below are for the x-axis, but the same applies for the y-axis.
  115. #
  116. # Each node either occupies ceil((node.right - node.right) / grid_width) or
  117. # ceil((node.right - node.right) / grid_width) + 1 grid squares. We will call
  118. # these the node's "footprint".
  119. #
  120. # ┌────────────────┐
  121. # minimum foot-print │ NODE │
  122. # └────────────────┘
  123. # │ grid 1 │ grid 2 │ grid 3 │ grid 4 | grid 5 |
  124. # ┌────────────────┐
  125. # maximum foot-print │ NODE │
  126. # └────────────────┘
  127. #
  128. # The algorithm will find the grid offset such that the number of nodes with
  129. # a _minimal_ footprint is _maximized_.
  130. # The scanline algorithm works as follows, we create events for both end points
  131. # of each node's footprint. The event have two properties,
  132. # - the coordinate: the amount the endpoint can move to the
  133. # left before it crosses a grid line
  134. # - the change: either +1 or -1, indicating whether crossing the grid line
  135. # would result in a minimal footprint node becoming a maximal footprint
  136. class Event:
  137. def __init__(self, coord: float, change: float):
  138. self.coord = coord
  139. self.change = change
  140. # create events for both the horizontal and vertical axis
  141. events_horizontal: List[Event] = []
  142. events_vertical: List[Event] = []
  143. for node in self._fixed_nodes:
  144. bounding_box = node.getBoundingBox()
  145. left = bounding_box.left - self._build_volume_bounding_box.left
  146. right = bounding_box.right - self._build_volume_bounding_box.left
  147. back = bounding_box.back - self._build_volume_bounding_box.back
  148. front = bounding_box.front - self._build_volume_bounding_box.back
  149. value_left = math.ceil(left / self._grid_width) * self._grid_width - left
  150. value_right = math.ceil(right / self._grid_width) * self._grid_width - right
  151. value_back = math.ceil(back / self._grid_height) * self._grid_height - back
  152. value_front = math.ceil(front / self._grid_height) * self._grid_height - front
  153. # give nodes a weight according to their size. This
  154. # weight is heuristically chosen to be proportional to
  155. # the number of grid squares the node-boundary occupies
  156. weight = bounding_box.width + bounding_box.depth
  157. events_horizontal.append(Event(value_left, weight))
  158. events_horizontal.append(Event(value_right, -weight))
  159. events_vertical.append(Event(value_back, weight))
  160. events_vertical.append(Event(value_front, -weight))
  161. events_horizontal.sort(key=lambda event: event.coord)
  162. events_vertical.sort(key=lambda event: event.coord)
  163. def findOptimalShiftAxis(events: List[Event], interval: float) -> float:
  164. # executing the actual scanline algorithm
  165. # iteratively go through events (left to right) and keep track of the
  166. # current footprint. The optimal location is the one with the minimal
  167. # footprint. If there are multiple locations with the same minimal
  168. # footprint, the optimal location is the one with the largest range
  169. # between the left and right endpoint of the footprint.
  170. prev_offset = events[-1].coord - interval
  171. current_minimal_footprint_count = 0
  172. best_minimal_footprint_count = float('inf')
  173. best_offset_span = float('-inf')
  174. best_offset = 0.0
  175. for event in events:
  176. offset_span = event.coord - prev_offset
  177. if current_minimal_footprint_count < best_minimal_footprint_count or (
  178. current_minimal_footprint_count == best_minimal_footprint_count and offset_span > best_offset_span):
  179. best_minimal_footprint_count = current_minimal_footprint_count
  180. best_offset_span = offset_span
  181. best_offset = event.coord
  182. current_minimal_footprint_count += event.change
  183. prev_offset = event.coord
  184. return best_offset - best_offset_span * 0.5
  185. center_grid_x = 0.5 * self._grid_width
  186. center_grid_y = 0.5 * self._grid_height
  187. optimal_center_x = self._grid_width - findOptimalShiftAxis(events_horizontal, self._grid_width)
  188. optimal_center_y = self._grid_height - findOptimalShiftAxis(events_vertical, self._grid_height)
  189. self._offset_x = optimal_center_x - center_grid_x
  190. self._offset_y = optimal_center_y - center_grid_y
  191. def _moveNodeOnGrid(self, node: "SceneNode", grid_x: int, grid_y: int) -> "Operation.Operation":
  192. coord_grid_x, coord_grid_y = self._gridSpaceToCoordSpace(grid_x, grid_y)
  193. center_grid_x = coord_grid_x + (0.5 * self._grid_width)
  194. center_grid_y = coord_grid_y + (0.5 * self._grid_height)
  195. bounding_box = node.getBoundingBox()
  196. center_node_x = (bounding_box.left + bounding_box.right) * 0.5
  197. center_node_y = (bounding_box.back + bounding_box.front) * 0.5
  198. delta_x = center_grid_x - center_node_x
  199. delta_y = center_grid_y - center_node_y
  200. return TranslateOperation(node, Vector(delta_x, 0, delta_y))
  201. def _getGridCornerPoints(
  202. self,
  203. bounds: Union[AxisAlignedBox, Polygon],
  204. *,
  205. margin_x: float = 0.0,
  206. margin_y: float = 0.0
  207. ) -> Tuple[float, float, float, float]:
  208. if isinstance(bounds, AxisAlignedBox):
  209. coord_x1 = bounds.left - margin_x
  210. coord_x2 = bounds.right + margin_x
  211. coord_y1 = bounds.back - margin_y
  212. coord_y2 = bounds.front + margin_y
  213. elif isinstance(bounds, Polygon):
  214. coord_x1 = float('inf')
  215. coord_y1 = float('inf')
  216. coord_x2 = float('-inf')
  217. coord_y2 = float('-inf')
  218. for x, y in bounds.getPoints():
  219. coord_x1 = min(coord_x1, x)
  220. coord_y1 = min(coord_y1, y)
  221. coord_x2 = max(coord_x2, x)
  222. coord_y2 = max(coord_y2, y)
  223. else:
  224. raise TypeError("bounds must be either an AxisAlignedBox or a Polygon")
  225. coord_x1 -= margin_x
  226. coord_x2 += margin_x
  227. coord_y1 -= margin_y
  228. coord_y2 += margin_y
  229. grid_x1, grid_y1 = self._coordSpaceToGridSpace(coord_x1, coord_y1)
  230. grid_x2, grid_y2 = self._coordSpaceToGridSpace(coord_x2, coord_y2)
  231. return grid_x1, grid_y1, grid_x2, grid_y2
  232. def _intersectingGridIdxInclusive(self, bounds: Union[AxisAlignedBox, Polygon]) -> Set[Tuple[int, int]]:
  233. grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints(
  234. bounds,
  235. margin_x=-(self._margin_x + self._grid_round_margin_x) * 0.5,
  236. margin_y=-(self._margin_y + self._grid_round_margin_y) * 0.5,
  237. )
  238. grid_idx = set()
  239. for grid_x in range(math.floor(grid_x1), math.ceil(grid_x2)):
  240. for grid_y in range(math.floor(grid_y1), math.ceil(grid_y2)):
  241. grid_idx.add((grid_x, grid_y))
  242. return grid_idx
  243. def _intersectingGridIdxExclusive(self, bounds: Union[AxisAlignedBox, Polygon]) -> Set[Tuple[int, int]]:
  244. grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints(
  245. bounds,
  246. margin_x=(self._margin_x + self._grid_round_margin_x) * 0.5,
  247. margin_y=(self._margin_y + self._grid_round_margin_y) * 0.5,
  248. )
  249. grid_idx = set()
  250. for grid_x in range(math.ceil(grid_x1), math.floor(grid_x2)):
  251. for grid_y in range(math.ceil(grid_y1), math.floor(grid_y2)):
  252. grid_idx.add((grid_x, grid_y))
  253. return grid_idx
  254. def _gridSpaceToCoordSpace(self, x: float, y: float) -> Tuple[float, float]:
  255. grid_x = x * self._grid_width + self._build_volume_bounding_box.left + self._offset_x
  256. grid_y = y * self._grid_height + self._build_volume_bounding_box.back + self._offset_y
  257. return grid_x, grid_y
  258. def _coordSpaceToGridSpace(self, grid_x: float, grid_y: float) -> Tuple[float, float]:
  259. coord_x = (grid_x - self._build_volume_bounding_box.left - self._offset_x) / self._grid_width
  260. coord_y = (grid_y - self._build_volume_bounding_box.back - self._offset_y) / self._grid_height
  261. return coord_x, coord_y
  262. def _checkGridUnderDiscSpace(self, grid_x: int, grid_y: int) -> bool:
  263. left, back = self._gridSpaceToCoordSpace(grid_x, grid_y)
  264. right, front = self._gridSpaceToCoordSpace(grid_x + 1, grid_y + 1)
  265. corners = [(left, back), (right, back), (right, front), (left, front)]
  266. return all([self._checkPointUnderDiscSpace(x, y) for x, y in corners])
  267. def _checkPointUnderDiscSpace(self, x: float, y: float) -> bool:
  268. disc_x, disc_y = self._coordSpaceToDiscSpace(x, y)
  269. distance_to_center_squared = disc_x ** 2 + disc_y ** 2
  270. return distance_to_center_squared <= 1.0
  271. def _coordSpaceToDiscSpace(self, x: float, y: float) -> Tuple[float, float]:
  272. # Transform coordinate system to
  273. #
  274. # coord_build_plate_left = -1
  275. # | coord_build_plate_right = 1
  276. # v (0,1) v
  277. # ┌───────┬───────┐ < coord_build_plate_back = -1
  278. # │ │ │
  279. # │ │(0,0) │
  280. # (-1,0)├───────o───────┤(1,0)
  281. # │ │ │
  282. # │ │ │
  283. # └───────┴───────┘ < coord_build_plate_front = +1
  284. # (0,-1)
  285. disc_x = ((x - self._build_volume_bounding_box.left) / self._build_volume_bounding_box.width) * 2.0 - 1.0
  286. disc_y = ((y - self._build_volume_bounding_box.back) / self._build_volume_bounding_box.depth) * 2.0 - 1.0
  287. return disc_x, disc_y