GridArrange.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  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. self._grid_width = math.ceil(self._grid_width / 10) * 10
  32. self._grid_height = math.ceil(self._grid_height / 10) * 10
  33. self._offset_x = 0
  34. self._offset_y = 0
  35. self._findOptimalGridOffset()
  36. coord_initial_leftover_x = self._build_volume_bounding_box.right + 2 * self._grid_width
  37. coord_initial_leftover_y = (self._build_volume_bounding_box.back + self._build_volume_bounding_box.front) * 0.5
  38. self._initial_leftover_grid_x, self._initial_leftover_grid_y = self.coordSpaceToGridSpace(coord_initial_leftover_x, coord_initial_leftover_y)
  39. self._initial_leftover_grid_x = math.floor(self._initial_leftover_grid_x)
  40. self._initial_leftover_grid_y = math.floor(self._initial_leftover_grid_y)
  41. # Find grid indexes that intersect with fixed objects
  42. self._fixed_nodes_grid_ids = set()
  43. for node in self._fixed_nodes:
  44. self._fixed_nodes_grid_ids = self._fixed_nodes_grid_ids.union(
  45. self.intersectingGridIdxInclusive(node.getBoundingBox()))
  46. #grid indexes that are in disallowed area
  47. for polygon in self._build_volume.getDisallowedAreas():
  48. self._fixed_nodes_grid_ids = self._fixed_nodes_grid_ids.union(
  49. self._getIntersectingGridIdForPolygon(polygon))
  50. self._build_plate_grid_ids = self.intersectingGridIdxExclusive(self._build_volume_bounding_box)
  51. # Filter out the corner grid squares if the build plate shape is elliptic
  52. if self._build_volume.getShape() == "elliptic":
  53. self._build_plate_grid_ids = set(
  54. filter(lambda grid_id: self.checkGridUnderDiscSpace(grid_id[0], grid_id[1]),
  55. self._build_plate_grid_ids))
  56. self._allowed_grid_idx = self._build_plate_grid_ids.difference(self._fixed_nodes_grid_ids)
  57. def createGroupOperationForArrange(self, add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]:
  58. # Find the sequence in which items are placed
  59. coord_build_plate_center_x = self._build_volume_bounding_box.width * 0.5 + self._build_volume_bounding_box.left
  60. coord_build_plate_center_y = self._build_volume_bounding_box.depth * 0.5 + self._build_volume_bounding_box.back
  61. grid_build_plate_center_x, grid_build_plate_center_y = self.coordSpaceToGridSpace(coord_build_plate_center_x, coord_build_plate_center_y)
  62. sequence: List[Tuple[int, int]] = list(self._allowed_grid_idx)
  63. sequence.sort(key=lambda grid_id: (grid_build_plate_center_x - grid_id[0]) ** 2 + (
  64. grid_build_plate_center_y - grid_id[1]) ** 2)
  65. scene_root = Application.getInstance().getController().getScene().getRoot()
  66. grouped_operation = GroupedOperation()
  67. for grid_id, node in zip(sequence, self._nodes_to_arrange):
  68. if add_new_nodes_in_scene:
  69. grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
  70. grid_x, grid_y = grid_id
  71. operation = self._moveNodeOnGrid(node, grid_x, grid_y)
  72. grouped_operation.addOperation(operation)
  73. leftover_nodes = self._nodes_to_arrange[len(sequence):]
  74. left_over_grid_y = self._initial_leftover_grid_y
  75. for node in leftover_nodes:
  76. if add_new_nodes_in_scene:
  77. grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
  78. # find the first next grid position that isn't occupied by a fixed node
  79. while (self._initial_leftover_grid_x, left_over_grid_y) in self._fixed_nodes_grid_ids:
  80. left_over_grid_y = left_over_grid_y - 1
  81. operation = self._moveNodeOnGrid(node, self._initial_leftover_grid_x, left_over_grid_y)
  82. grouped_operation.addOperation(operation)
  83. left_over_grid_y = left_over_grid_y - 1
  84. return grouped_operation, len(leftover_nodes)
  85. def _findOptimalGridOffset(self):
  86. if len(self._fixed_nodes) == 0:
  87. self._offset_x = 0
  88. self._offset_y = 0
  89. return
  90. if len(self._fixed_nodes) == 1:
  91. center_grid_x = 0.5 * self._grid_width + self._build_volume_bounding_box.left
  92. center_grid_y = 0.5 * self._grid_height + self._build_volume_bounding_box.back
  93. bounding_box = self._fixed_nodes[0].getBoundingBox()
  94. center_node_x = (bounding_box.left + bounding_box.right) * 0.5
  95. center_node_y = (bounding_box.back + bounding_box.front) * 0.5
  96. self._offset_x = center_node_x - center_grid_x
  97. self._offset_y = center_node_y - center_grid_y
  98. return
  99. # If there are multiple fixed nodes, an optimal solution is not always possible
  100. # We will try to find an offset that minimizes the number of grid intersections
  101. # with fixed nodes. The algorithm below achieves this by utilizing a scanline
  102. # algorithm. In this algorithm each axis is solved separately as offsetting
  103. # is completely independent in each axis. The comments explaining the algorithm
  104. # below are for the x-axis, but the same applies for the y-axis.
  105. #
  106. # Each node either occupies ceil((node.right - node.right) / grid_width) or
  107. # ceil((node.right - node.right) / grid_width) + 1 grid squares. We will call
  108. # these the node's "footprint".
  109. #
  110. # ┌────────────────┐
  111. # minimum food print │ NODE │
  112. # └────────────────┘
  113. # │ grid 1 │ grid 2 │ grid 3 │ grid 4 | grid 5 |
  114. # ┌────────────────┐
  115. # maximum food print │ NODE │
  116. # └────────────────┘
  117. #
  118. # The algorithm will find the grid offset such that the number of nodes with
  119. # a _minimal_ footprint is _maximized_.
  120. # The scanline algorithm works as follows, we create events for both end points
  121. # of each node's footprint. The event have two properties,
  122. # - the coordinate: the amount the endpoint can move to the
  123. # left before it crosses a grid line
  124. # - the change: either +1 or -1, indicating whether crossing the grid line
  125. # would result in a minimal footprint node becoming a maximal footprint
  126. class Event:
  127. def __init__(self, coord: float, change: float):
  128. self.coord = coord
  129. self.change = change
  130. # create events for both the horizontal and vertical axis
  131. events_horizontal: List[Event] = []
  132. events_vertical: List[Event] = []
  133. for node in self._fixed_nodes:
  134. bounding_box = node.getBoundingBox()
  135. left = bounding_box.left - self._build_volume_bounding_box.left
  136. right = bounding_box.right - self._build_volume_bounding_box.left
  137. back = bounding_box.back - self._build_volume_bounding_box.back
  138. front = bounding_box.front - self._build_volume_bounding_box.back
  139. value_left = math.ceil(left / self._grid_width) * self._grid_width - left
  140. value_right = math.ceil(right / self._grid_width) * self._grid_width - right
  141. value_back = math.ceil(back / self._grid_height) * self._grid_height - back
  142. value_front = math.ceil(front / self._grid_height) * self._grid_height - front
  143. # give nodes a weight according to their size. This
  144. # weight is heuristically chosen to be proportional to
  145. # the number of grid squares the node-boundary occupies
  146. weight = bounding_box.width + bounding_box.depth
  147. events_horizontal.append(Event(value_left, weight))
  148. events_horizontal.append(Event(value_right, -weight))
  149. events_vertical.append(Event(value_back, weight))
  150. events_vertical.append(Event(value_front, -weight))
  151. events_horizontal.sort(key=lambda event: event.coord)
  152. events_vertical.sort(key=lambda event: event.coord)
  153. def findOptimalShiftAxis(events: List[Event], interval: float) -> float:
  154. # executing the actual scanline algorithm
  155. # iteratively go through events (left to right) and keep track of the
  156. # current footprint. The optimal location is the one with the minimal
  157. # footprint. If there are multiple locations with the same minimal
  158. # footprint, the optimal location is the one with the largest range
  159. # between the left and right endpoint of the footprint.
  160. prev_offset = events[-1].coord - interval
  161. current_minimal_footprint_count = 0
  162. best_minimal_footprint_count = float('inf')
  163. best_offset_span = float('-inf')
  164. best_offset = 0.0
  165. for event in events:
  166. offset_span = event.coord - prev_offset
  167. if current_minimal_footprint_count < best_minimal_footprint_count or (
  168. current_minimal_footprint_count == best_minimal_footprint_count and offset_span > best_offset_span):
  169. best_minimal_footprint_count = current_minimal_footprint_count
  170. best_offset_span = offset_span
  171. best_offset = event.coord
  172. current_minimal_footprint_count += event.change
  173. prev_offset = event.coord
  174. return best_offset - best_offset_span * 0.5
  175. center_grid_x = 0.5 * self._grid_width
  176. center_grid_y = 0.5 * self._grid_height
  177. optimal_center_x = self._grid_width - findOptimalShiftAxis(events_horizontal, self._grid_width)
  178. optimal_center_y = self._grid_height - findOptimalShiftAxis(events_vertical, self._grid_height)
  179. self._offset_x = optimal_center_x - center_grid_x
  180. self._offset_y = optimal_center_y - center_grid_y
  181. def _moveNodeOnGrid(self, node: "SceneNode", grid_x: int, grid_y: int) -> "Operation.Operation":
  182. coord_grid_x, coord_grid_y = self._gridSpaceToCoordSpace(grid_x, grid_y)
  183. center_grid_x = coord_grid_x + (0.5 * self._grid_width)
  184. center_grid_y = coord_grid_y + (0.5 * self._grid_height)
  185. bounding_box = node.getBoundingBox()
  186. center_node_x = (bounding_box.left + bounding_box.right) * 0.5
  187. center_node_y = (bounding_box.back + bounding_box.front) * 0.5
  188. delta_x = center_grid_x - center_node_x
  189. delta_y = center_grid_y - center_node_y
  190. return TranslateOperation(node, Vector(delta_x, 0, delta_y))
  191. def _getGridCornerPoints(self, bounding_box: "BoundingVolume") -> Tuple[float, float, float, float]:
  192. coord_x1 = bounding_box.left
  193. coord_x2 = bounding_box.right
  194. coord_y1 = bounding_box.back
  195. coord_y2 = bounding_box.front
  196. grid_x1, grid_y1 = self.coordSpaceToGridSpace(coord_x1, coord_y1)
  197. grid_x2, grid_y2 = self.coordSpaceToGridSpace(coord_x2, coord_y2)
  198. return grid_x1, grid_y1, grid_x2, grid_y2
  199. def _getIntersectingGridIdForPolygon(self, polygon)-> Set[Tuple[int, int]]:
  200. # (x0, y0)
  201. # |
  202. # v
  203. # ┌─────────────┐
  204. # │ │
  205. # │ │
  206. # └─────────────┘ < (x1, y1)
  207. x0 = float('inf')
  208. y0 = float('inf')
  209. x1 = float('-inf')
  210. y1 = float('-inf')
  211. grid_idx = set()
  212. for [x, y] in polygon.getPoints():
  213. x0 = min(x0, x)
  214. y0 = min(y0, y)
  215. x1 = max(x1, x)
  216. y1 = max(y1, y)
  217. grid_x1, grid_y1 = self.coordSpaceToGridSpace(x0, y0)
  218. grid_x2, grid_y2 = self.coordSpaceToGridSpace(x1, y1)
  219. for grid_x in range(math.floor(grid_x1), math.ceil(grid_x2)):
  220. for grid_y in range(math.floor(grid_y1), math.ceil(grid_y2)):
  221. grid_idx.add((grid_x, grid_y))
  222. return grid_idx
  223. def intersectingGridIdxInclusive(self, bounding_box: "BoundingVolume") -> Set[Tuple[int, int]]:
  224. grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints(bounding_box)
  225. grid_idx = set()
  226. for grid_x in range(math.floor(grid_x1), math.ceil(grid_x2)):
  227. for grid_y in range(math.floor(grid_y1), math.ceil(grid_y2)):
  228. grid_idx.add((grid_x, grid_y))
  229. return grid_idx
  230. def intersectingGridIdxExclusive(self, bounding_box: "BoundingVolume") -> Set[Tuple[int, int]]:
  231. grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints(bounding_box)
  232. grid_idx = set()
  233. for grid_x in range(math.ceil(grid_x1), math.floor(grid_x2)):
  234. for grid_y in range(math.ceil(grid_y1), math.floor(grid_y2)):
  235. grid_idx.add((grid_x, grid_y))
  236. return grid_idx
  237. def _gridSpaceToCoordSpace(self, x: float, y: float) -> Tuple[float, float]:
  238. grid_x = x * self._grid_width + self._build_volume_bounding_box.left + self._offset_x
  239. grid_y = y * self._grid_height + self._build_volume_bounding_box.back + self._offset_y
  240. return grid_x, grid_y
  241. def coordSpaceToGridSpace(self, grid_x: float, grid_y: float) -> Tuple[float, float]:
  242. coord_x = (grid_x - self._build_volume_bounding_box.left - self._offset_x) / self._grid_width
  243. coord_y = (grid_y - self._build_volume_bounding_box.back - self._offset_y) / self._grid_height
  244. return coord_x, coord_y
  245. def checkGridUnderDiscSpace(self, grid_x: int, grid_y: int) -> bool:
  246. left, back = self._gridSpaceToCoordSpace(grid_x, grid_y)
  247. right, front = self._gridSpaceToCoordSpace(grid_x + 1, grid_y + 1)
  248. corners = [(left, back), (right, back), (right, front), (left, front)]
  249. return all([self.checkPointUnderDiscSpace(x, y) for x, y in corners])
  250. def checkPointUnderDiscSpace(self, x: float, y: float) -> bool:
  251. disc_x, disc_y = self.coordSpaceToDiscSpace(x, y)
  252. distance_to_center_squared = disc_x ** 2 + disc_y ** 2
  253. return distance_to_center_squared <= 1.0
  254. def coordSpaceToDiscSpace(self, x: float, y: float) -> Tuple[float, float]:
  255. # Transform coordinate system to
  256. #
  257. # coord_build_plate_left = -1
  258. # | coord_build_plate_right = 1
  259. # v (0,1) v
  260. # ┌───────┬───────┐ < coord_build_plate_back = -1
  261. # │ │ │
  262. # │ │(0,0) │
  263. # (-1,0)├───────o───────┤(1,0)
  264. # │ │ │
  265. # │ │ │
  266. # └───────┴───────┘ < coord_build_plate_front = +1
  267. # (0,-1)
  268. disc_x = ((x - self._build_volume_bounding_box.left) / self._build_volume_bounding_box.width) * 2.0 - 1.0
  269. disc_y = ((y - self._build_volume_bounding_box.back) / self._build_volume_bounding_box.depth) * 2.0 - 1.0
  270. return disc_x, disc_y
  271. def _drawDebugSvg(self):
  272. with open("Builvolume_test.svg", "w") as f:
  273. build_volume_bounding_box = self._build_volume_bounding_box
  274. f.write(
  275. f"<svg xmlns='http://www.w3.org/2000/svg' viewBox='{build_volume_bounding_box.left - 100} {build_volume_bounding_box.back - 100} {build_volume_bounding_box.width + 200} {build_volume_bounding_box.depth + 200}'>\n")
  276. if self._build_volume.getShape() == "elliptic":
  277. f.write(
  278. f"""
  279. <ellipse
  280. cx='{(build_volume_bounding_box.left + build_volume_bounding_box.right) * 0.5}'
  281. cy='{(build_volume_bounding_box.back + build_volume_bounding_box.front) * 0.5}'
  282. rx='{build_volume_bounding_box.width * 0.5}'
  283. ry='{build_volume_bounding_box.depth * 0.5}'
  284. fill=\"lightgrey\"
  285. />
  286. """)
  287. else:
  288. f.write(
  289. f"""
  290. <rect
  291. x='{build_volume_bounding_box.left}'
  292. y='{build_volume_bounding_box.back}'
  293. width='{build_volume_bounding_box.width}'
  294. height='{build_volume_bounding_box.depth}'
  295. fill=\"lightgrey\"
  296. />
  297. """)
  298. for polygon in self._build_volume.getDisallowedAreas():
  299. # Extract individual points and convert them to tuples
  300. path_data = ""
  301. for [x,y] in polygon.getPoints():
  302. path_data += f"{x},{y} "
  303. f.write(
  304. f"""
  305. <polygon
  306. points='{path_data}'
  307. fill='#ff648888'
  308. stroke='black'
  309. stroke-width='2'
  310. />
  311. """)
  312. for grid_x in range(-10, 100):
  313. for grid_y in range(-10, 100):
  314. if (grid_x, grid_y) in self._allowed_grid_idx:
  315. fill_color = "rgba(0, 255, 0, 0.5)"
  316. elif (grid_x, grid_y) in self._build_plate_grid_ids:
  317. fill_color = "rgba(255, 165, 0, 0.5)"
  318. else:
  319. fill_color = "rgba(255, 0, 0, 0.5)"
  320. coord_grid_x, coord_grid_y = self._gridSpaceToCoordSpace(grid_x, grid_y)
  321. f.write(
  322. f"""
  323. <rect
  324. x="{coord_grid_x + self._margin_x * 0.5}"
  325. y="{coord_grid_y + self._margin_y * 0.5}"
  326. width="{self._grid_width - self._margin_x}"
  327. height="{self._grid_height - self._margin_y}"
  328. fill="{fill_color}"
  329. stroke="black"
  330. />
  331. """)
  332. f.write(f"""
  333. <text
  334. font-size="4"
  335. text-anchor="middle"
  336. alignment-baseline="middle"
  337. x="{coord_grid_x + self._grid_width * 0.5}"
  338. y="{coord_grid_y + self._grid_height * 0.5}"
  339. >
  340. {grid_x},{grid_y}
  341. </text>
  342. """)
  343. for node in self._fixed_nodes:
  344. bounding_box = node.getBoundingBox()
  345. f.write(f"""
  346. <rect
  347. x="{bounding_box.left}"
  348. y="{bounding_box.back}"
  349. width="{bounding_box.width}"
  350. height="{bounding_box.depth}"
  351. fill="red"
  352. />
  353. """)
  354. f.write(f"""
  355. <circle
  356. cx="{self._offset_x}"
  357. cy="{self._offset_y}"
  358. r="2"
  359. stroke="red"
  360. fill="none"
  361. />""")
  362. # coord_build_plate_center_x = self._build_volume_bounding_box.width * 0.5 + self._build_volume_bounding_box.left
  363. # coord_build_plate_center_y = self._build_volume_bounding_box.depth * 0.5 + self._build_volume_bounding_box.back
  364. # f.write(f"""
  365. # <circle
  366. # cx="{coord_build_plate_center_x}"
  367. # cy="{coord_build_plate_center_y}"
  368. # r="2"
  369. # stroke="blue"
  370. # fill="none"
  371. # />""")
  372. f.write(f"</svg>")