GridArrange.py 16 KB

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