ArrangeObjectsAllBuildPlatesJob.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. # Copyright (c) 2017 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. from UM.Job import Job
  4. from UM.Scene.SceneNode import SceneNode
  5. from UM.Math.Vector import Vector
  6. from UM.Operations.TranslateOperation import TranslateOperation
  7. from UM.Operations.GroupedOperation import GroupedOperation
  8. from UM.Message import Message
  9. from UM.i18n import i18nCatalog
  10. i18n_catalog = i18nCatalog("cura")
  11. from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
  12. from cura.Arranging.Arrange import Arrange
  13. from cura.Arranging.ShapeArray import ShapeArray
  14. from typing import List
  15. class ArrangeArray:
  16. def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]):
  17. self._x = x
  18. self._y = y
  19. self._fixed_nodes = fixed_nodes
  20. self._count = 0
  21. self._first_empty = None
  22. self._has_empty = False
  23. self._arrange = []
  24. def _update_first_empty(self):
  25. for i, a in enumerate(self._arrange):
  26. if a.isEmpty:
  27. self._first_empty = i
  28. self._has_empty = True
  29. return
  30. self._first_empty = None
  31. self._has_empty = False
  32. def add(self):
  33. new_arrange = Arrange.create(x = self._x, y = self._y, fixed_nodes = self._fixed_nodes)
  34. self._arrange.append(new_arrange)
  35. self._count += 1
  36. self._update_first_empty()
  37. def count(self):
  38. return self._count
  39. def get(self, index):
  40. return self._arrange[index]
  41. def getFirstEmpty(self):
  42. if not self._is_empty:
  43. self.add()
  44. return self._arrange[self._first_empty]
  45. class ArrangeObjectsAllBuildPlatesJob(Job):
  46. def __init__(self, nodes: List[SceneNode], min_offset = 8):
  47. super().__init__()
  48. self._nodes = nodes
  49. self._min_offset = min_offset
  50. def run(self):
  51. status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"),
  52. lifetime = 0,
  53. dismissable=False,
  54. progress = 0,
  55. title = i18n_catalog.i18nc("@info:title", "Finding Location"))
  56. status_message.show()
  57. # Collect nodes to be placed
  58. nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
  59. for node in self._nodes:
  60. offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset)
  61. nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr))
  62. # Sort the nodes with the biggest area first.
  63. nodes_arr.sort(key=lambda item: item[0])
  64. nodes_arr.reverse()
  65. x, y = 200, 200
  66. arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = [])
  67. arrange_array.add()
  68. # Place nodes one at a time
  69. start_priority = 0
  70. grouped_operation = GroupedOperation()
  71. found_solution_for_all = True
  72. left_over_nodes = [] # nodes that do not fit on an empty build plate
  73. for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
  74. # For performance reasons, we assume that when a location does not fit,
  75. # it will also not fit for the next object (while what can be untrue).
  76. # We also skip possibilities by slicing through the possibilities (step = 10)
  77. try_placement = True
  78. current_build_plate_number = 0 # always start with the first one
  79. # # Only for first build plate
  80. # if last_size == size and last_build_plate_number == current_build_plate_number:
  81. # # This optimization works if many of the objects have the same size
  82. # # Continue with same build plate number
  83. # start_priority = last_priority
  84. # else:
  85. # start_priority = 0
  86. while try_placement:
  87. # make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects
  88. while current_build_plate_number >= arrange_array.count():
  89. arrange_array.add()
  90. arranger = arrange_array.get(current_build_plate_number)
  91. best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
  92. x, y = best_spot.x, best_spot.y
  93. node.removeDecorator(ZOffsetDecorator)
  94. if node.getBoundingBox():
  95. center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
  96. else:
  97. center_y = 0
  98. if x is not None: # We could find a place
  99. arranger.place(x, y, hull_shape_arr) # place the object in the arranger
  100. node.callDecoration("setBuildPlateNumber", current_build_plate_number)
  101. grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
  102. try_placement = False
  103. else:
  104. # very naive, because we skip to the next build plate if one model doesn't fit.
  105. if arranger.isEmpty:
  106. # apparently we can never place this object
  107. left_over_nodes.append(node)
  108. try_placement = False
  109. else:
  110. # try next build plate
  111. current_build_plate_number += 1
  112. try_placement = True
  113. status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
  114. Job.yieldThread()
  115. for node in left_over_nodes:
  116. node.callDecoration("setBuildPlateNumber", -1) # these are not on any build plate
  117. found_solution_for_all = False
  118. grouped_operation.push()
  119. status_message.hide()
  120. if not found_solution_for_all:
  121. no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"),
  122. title = i18n_catalog.i18nc("@info:title", "Can't Find Location"))
  123. no_full_solution_message.show()