ArrangeObjectsAllBuildPlatesJob.py 6.3 KB

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