GridArrange.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  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. self._build_plate_grid_ids = self.intersectingGridIdxExclusive(self._build_volume_bounding_box)
  47. # Filter out the corner grid squares if the build plate shape is elliptic
  48. if self._build_volume.getShape() == "elliptic":
  49. self._build_plate_grid_ids = set(
  50. filter(lambda grid_id: self.checkGridUnderDiscSpace(grid_id[0], grid_id[1]),
  51. self._build_plate_grid_ids))
  52. self._allowed_grid_idx = self._build_plate_grid_ids.difference(self._fixed_nodes_grid_ids)
  53. def createGroupOperationForArrange(self, add_new_nodes_in_scene: bool = True) -> Tuple[GroupedOperation, int]:
  54. # Find the sequence in which items are placed
  55. coord_build_plate_center_x = self._build_volume_bounding_box.width * 0.5 + self._build_volume_bounding_box.left
  56. coord_build_plate_center_y = self._build_volume_bounding_box.depth * 0.5 + self._build_volume_bounding_box.back
  57. grid_build_plate_center_x, grid_build_plate_center_y = self.coordSpaceToGridSpace(coord_build_plate_center_x, coord_build_plate_center_y)
  58. sequence: List[Tuple[int, int]] = list(self._allowed_grid_idx)
  59. sequence.sort(key=lambda grid_id: (grid_build_plate_center_x - grid_id[0]) ** 2 + (
  60. grid_build_plate_center_y - grid_id[1]) ** 2)
  61. scene_root = Application.getInstance().getController().getScene().getRoot()
  62. grouped_operation = GroupedOperation()
  63. for grid_id, node in zip(sequence, self._nodes_to_arrange):
  64. grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
  65. grid_x, grid_y = grid_id
  66. operation = self._moveNodeOnGrid(node, grid_x, grid_y)
  67. grouped_operation.addOperation(operation)
  68. leftover_nodes = self._nodes_to_arrange[len(sequence):]
  69. left_over_grid_y = self._initial_leftover_grid_y
  70. for node in leftover_nodes:
  71. if add_new_nodes_in_scene:
  72. grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
  73. # find the first next grid position that isn't occupied by a fixed node
  74. while (self._initial_leftover_grid_x, left_over_grid_y) in self._fixed_nodes_grid_ids:
  75. left_over_grid_y = left_over_grid_y - 1
  76. operation = self._moveNodeOnGrid(node, self._initial_leftover_grid_x, left_over_grid_y)
  77. grouped_operation.addOperation(operation)
  78. left_over_grid_y = left_over_grid_y - 1
  79. return grouped_operation, len(leftover_nodes)
  80. def _findOptimalGridOffset(self):
  81. if len(self._fixed_nodes) == 0:
  82. self._offset_x = 0
  83. self._offset_y = 0
  84. return
  85. if len(self._fixed_nodes) == 1:
  86. center_grid_x = 0.5 * self._grid_width + self._build_volume_bounding_box.left
  87. center_grid_y = 0.5 * self._grid_height + self._build_volume_bounding_box.back
  88. bounding_box = self._fixed_nodes[0].getBoundingBox()
  89. center_node_x = (bounding_box.left + bounding_box.right) * 0.5
  90. center_node_y = (bounding_box.back + bounding_box.front) * 0.5
  91. self._offset_x = center_node_x - center_grid_x
  92. self._offset_y = center_node_y - center_grid_y
  93. return
  94. class Event:
  95. def __init__(self, coord: float, change: float):
  96. self.coord = coord
  97. self.change = change
  98. events_horizontal: List[Event] = []
  99. events_vertical: List[Event] = []
  100. for node in self._fixed_nodes:
  101. bounding_box = node.getBoundingBox()
  102. left = bounding_box.left - self._build_volume_bounding_box.left
  103. right = bounding_box.right - self._build_volume_bounding_box.left
  104. back = bounding_box.back - self._build_volume_bounding_box.back
  105. front = bounding_box.front - self._build_volume_bounding_box.back
  106. value_left = math.ceil(left / self._grid_width) * self._grid_width - left
  107. value_right = math.ceil(right / self._grid_width) * self._grid_width - right
  108. value_back = math.ceil(back / self._grid_height) * self._grid_height - back
  109. value_front = math.ceil(front / self._grid_height) * self._grid_height - front
  110. # give nodes a weight according to their size. This
  111. # weight is heuristically chosen to be proportional to
  112. # the number of grid squares the node-boundary occupies
  113. weight = bounding_box.width + bounding_box.depth
  114. events_horizontal.append(Event(value_left, weight))
  115. events_horizontal.append(Event(value_right, -weight))
  116. events_vertical.append(Event(value_back, weight))
  117. events_vertical.append(Event(value_front, -weight))
  118. events_horizontal.sort(key=lambda event: event.coord)
  119. events_vertical.sort(key=lambda event: event.coord)
  120. def findOptimalOffsetAxis(events: List[Event], interval: float) -> float:
  121. prev_coord = events[-1].coord - interval
  122. current_offset = 0
  123. best_offset = float('inf')
  124. best_coord_length = float('-inf')
  125. best_coord = 0.0
  126. for event in events:
  127. coord_length = event.coord - prev_coord
  128. if current_offset < best_offset or (current_offset == best_offset and coord_length > best_coord_length):
  129. best_offset = current_offset
  130. best_coord_length = coord_length
  131. best_coord = event.coord
  132. current_offset += event.change
  133. prev_coord = event.coord
  134. return best_coord - best_coord_length * 0.5
  135. center_grid_x = 0.5 * self._grid_width
  136. center_grid_y = 0.5 * self._grid_height
  137. optimal_center_x = self._grid_width - findOptimalOffsetAxis(events_horizontal, self._grid_width)
  138. optimal_center_y = self._grid_height - findOptimalOffsetAxis(events_vertical, self._grid_height)
  139. self._offset_x = optimal_center_x - center_grid_x
  140. self._offset_y = optimal_center_y - center_grid_y
  141. def _moveNodeOnGrid(self, node: "SceneNode", grid_x: int, grid_y: int) -> "Operation.Operation":
  142. coord_grid_x, coord_grid_y = self._gridSpaceToCoordSpace(grid_x, grid_y)
  143. center_grid_x = coord_grid_x + (0.5 * self._grid_width)
  144. center_grid_y = coord_grid_y + (0.5 * self._grid_height)
  145. bounding_box = node.getBoundingBox()
  146. center_node_x = (bounding_box.left + bounding_box.right) * 0.5
  147. center_node_y = (bounding_box.back + bounding_box.front) * 0.5
  148. delta_x = center_grid_x - center_node_x
  149. delta_y = center_grid_y - center_node_y
  150. return TranslateOperation(node, Vector(delta_x, 0, delta_y))
  151. def _getGridCornerPoints(self, bounding_box: "BoundingVolume") -> Tuple[float, float, float, float]:
  152. coord_x1 = bounding_box.left
  153. coord_x2 = bounding_box.right
  154. coord_y1 = bounding_box.back
  155. coord_y2 = bounding_box.front
  156. grid_x1, grid_y1 = self.coordSpaceToGridSpace(coord_x1, coord_y1)
  157. grid_x2, grid_y2 = self.coordSpaceToGridSpace(coord_x2, coord_y2)
  158. return grid_x1, grid_y1, grid_x2, grid_y2
  159. def intersectingGridIdxInclusive(self, bounding_box: "BoundingVolume") -> Set[Tuple[int, int]]:
  160. grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints(bounding_box)
  161. grid_idx = set()
  162. for grid_x in range(math.floor(grid_x1), math.ceil(grid_x2)):
  163. for grid_y in range(math.floor(grid_y1), math.ceil(grid_y2)):
  164. grid_idx.add((grid_x, grid_y))
  165. return grid_idx
  166. def intersectingGridIdxExclusive(self, bounding_box: "BoundingVolume") -> Set[Tuple[int, int]]:
  167. grid_x1, grid_y1, grid_x2, grid_y2 = self._getGridCornerPoints(bounding_box)
  168. grid_idx = set()
  169. for grid_x in range(math.ceil(grid_x1), math.floor(grid_x2)):
  170. for grid_y in range(math.ceil(grid_y1), math.floor(grid_y2)):
  171. grid_idx.add((grid_x, grid_y))
  172. return grid_idx
  173. def _gridSpaceToCoordSpace(self, x: float, y: float) -> Tuple[float, float]:
  174. grid_x = x * self._grid_width + self._build_volume_bounding_box.left + self._offset_x
  175. grid_y = y * self._grid_height + self._build_volume_bounding_box.back + self._offset_y
  176. return grid_x, grid_y
  177. def coordSpaceToGridSpace(self, grid_x: float, grid_y: float) -> Tuple[float, float]:
  178. coord_x = (grid_x - self._build_volume_bounding_box.left - self._offset_x) / self._grid_width
  179. coord_y = (grid_y - self._build_volume_bounding_box.back - self._offset_y) / self._grid_height
  180. return coord_x, coord_y
  181. def checkGridUnderDiscSpace(self, grid_x: int, grid_y: int) -> bool:
  182. left, back = self._gridSpaceToCoordSpace(grid_x, grid_y)
  183. right, front = self._gridSpaceToCoordSpace(grid_x + 1, grid_y + 1)
  184. corners = [(left, back), (right, back), (right, front), (left, front)]
  185. return all([self.checkPointUnderDiscSpace(x, y) for x, y in corners])
  186. def checkPointUnderDiscSpace(self, x: float, y: float) -> bool:
  187. disc_x, disc_y = self.coordSpaceToDiscSpace(x, y)
  188. distance_to_center_squared = disc_x ** 2 + disc_y ** 2
  189. return distance_to_center_squared <= 1.0
  190. def coordSpaceToDiscSpace(self, x: float, y: float) -> Tuple[float, float]:
  191. # Transform coordinate system to
  192. #
  193. # coord_build_plate_left = -1
  194. # | coord_build_plate_right = 1
  195. # v (0,1) v
  196. # ┌───────┬───────┐ < coord_build_plate_back = -1
  197. # │ │ │
  198. # │ │(0,0) │
  199. # (-1,0)├───────o───────┤(1,0)
  200. # │ │ │
  201. # │ │ │
  202. # └───────┴───────┘ < coord_build_plate_front = +1
  203. # (0,-1)
  204. disc_x = ((x - self._build_volume_bounding_box.left) / self._build_volume_bounding_box.width) * 2.0 - 1.0
  205. disc_y = ((y - self._build_volume_bounding_box.back) / self._build_volume_bounding_box.depth) * 2.0 - 1.0
  206. return disc_x, disc_y
  207. def _drawDebugSvg(self):
  208. with open("Builvolume_test.svg", "w") as f:
  209. build_volume_bounding_box = self._build_volume_bounding_box
  210. f.write(
  211. 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")
  212. if self._build_volume.getShape() == "elliptic":
  213. f.write(
  214. f"""
  215. <ellipse
  216. cx='{(build_volume_bounding_box.left + build_volume_bounding_box.right) * 0.5}'
  217. cy='{(build_volume_bounding_box.back + build_volume_bounding_box.front) * 0.5}'
  218. rx='{build_volume_bounding_box.width * 0.5}'
  219. ry='{build_volume_bounding_box.depth * 0.5}'
  220. fill=\"lightgrey\"
  221. />
  222. """)
  223. else:
  224. f.write(
  225. f"""
  226. <rect
  227. x='{build_volume_bounding_box.left}'
  228. y='{build_volume_bounding_box.back}'
  229. width='{build_volume_bounding_box.width}'
  230. height='{build_volume_bounding_box.depth}'
  231. fill=\"lightgrey\"
  232. />
  233. """)
  234. for grid_x in range(-10, 10):
  235. for grid_y in range(-10, 10):
  236. if (grid_x, grid_y) in self._allowed_grid_idx:
  237. fill_color = "rgba(0, 255, 0, 0.5)"
  238. elif (grid_x, grid_y) in self._build_plate_grid_ids:
  239. fill_color = "rgba(255, 165, 0, 0.5)"
  240. else:
  241. fill_color = "rgba(255, 0, 0, 0.5)"
  242. coord_grid_x, coord_grid_y = self._gridSpaceToCoordSpace(grid_x, grid_y)
  243. f.write(
  244. f"""
  245. <rect
  246. x="{coord_grid_x + self._margin_x * 0.5}"
  247. y="{coord_grid_y + self._margin_y * 0.5}"
  248. width="{self._grid_width - self._margin_x}"
  249. height="{self._grid_height - self._margin_y}"
  250. fill="{fill_color}"
  251. stroke="black"
  252. />
  253. """)
  254. f.write(f"""
  255. <text
  256. font-size="4"
  257. text-anchor="middle"
  258. alignment-baseline="middle"
  259. x="{coord_grid_x + self._grid_width * 0.5}"
  260. y="{coord_grid_y + self._grid_height * 0.5}"
  261. >
  262. {grid_x},{grid_y}
  263. </text>
  264. """)
  265. for node in self._fixed_nodes:
  266. bounding_box = node.getBoundingBox()
  267. f.write(f"""
  268. <rect
  269. x="{bounding_box.left}"
  270. y="{bounding_box.back}"
  271. width="{bounding_box.width}"
  272. height="{bounding_box.depth}"
  273. fill="red"
  274. />
  275. """)
  276. f.write(f"""
  277. <circle
  278. cx="{self._offset_x}"
  279. cy="{self._offset_y}"
  280. r="2"
  281. stroke="red"
  282. fill="none"
  283. />""")
  284. # coord_build_plate_center_x = self._build_volume_bounding_box.width * 0.5 + self._build_volume_bounding_box.left
  285. # coord_build_plate_center_y = self._build_volume_bounding_box.depth * 0.5 + self._build_volume_bounding_box.back
  286. # f.write(f"""
  287. # <circle
  288. # cx="{coord_build_plate_center_x}"
  289. # cy="{coord_build_plate_center_y}"
  290. # r="2"
  291. # stroke="blue"
  292. # fill="none"
  293. # />""")
  294. f.write(f"</svg>")