ArrangeObjectsAllBuildPlatesJob.py 6.1 KB

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