ArrangeObjectsJob.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. # Copyright (c) 2019 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import numpy
  4. from PyQt5.QtCore import QCoreApplication
  5. from UM.Application import Application
  6. from UM.Job import Job
  7. from UM.Math.Matrix import Matrix
  8. from UM.Math.Polygon import Polygon
  9. from UM.Math.Quaternion import Quaternion
  10. from UM.Operations.RotateOperation import RotateOperation
  11. from UM.Scene.SceneNode import SceneNode
  12. from UM.Math.Vector import Vector
  13. from UM.Operations.TranslateOperation import TranslateOperation
  14. from UM.Operations.GroupedOperation import GroupedOperation
  15. from UM.Logger import Logger
  16. from UM.Message import Message
  17. from UM.i18n import i18nCatalog
  18. i18n_catalog = i18nCatalog("cura")
  19. from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
  20. from cura.Arranging.Arrange import Arrange
  21. from cura.Arranging.ShapeArray import ShapeArray
  22. from typing import List
  23. from pynest2d import *
  24. class ArrangeObjectsJob(Job):
  25. def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8) -> None:
  26. super().__init__()
  27. self._nodes = nodes
  28. self._fixed_nodes = fixed_nodes
  29. self._min_offset = min_offset
  30. def run(self):
  31. status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"),
  32. lifetime = 0,
  33. dismissable=False,
  34. progress = 0,
  35. title = i18n_catalog.i18nc("@info:title", "Finding Location"))
  36. status_message.show()
  37. global_container_stack = Application.getInstance().getGlobalContainerStack()
  38. machine_width = global_container_stack.getProperty("machine_width", "value")
  39. machine_depth = global_container_stack.getProperty("machine_depth", "value")
  40. factor = 10000
  41. build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor)
  42. # Add all the items we want to arrange
  43. node_items = []
  44. for node in self._nodes:
  45. hull_polygon = node.callDecoration("getConvexHull")
  46. converted_points = []
  47. for point in hull_polygon.getPoints():
  48. converted_points.append(Point(point[0] * factor, point[1] * factor))
  49. item = Item(converted_points)
  50. node_items.append(item)
  51. # Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas)
  52. half_machine_width = 0.5 * machine_width - 1
  53. half_machine_depth = 0.5 * machine_depth - 1
  54. build_plate_polygon = Polygon(numpy.array([
  55. [half_machine_width, -half_machine_depth],
  56. [-half_machine_width, -half_machine_depth],
  57. [-half_machine_width, half_machine_depth],
  58. [half_machine_width, half_machine_depth]
  59. ], numpy.float32))
  60. build_volume = Application.getInstance().getBuildVolume()
  61. disallowed_areas = build_volume.getDisallowedAreas()
  62. num_disallowed_areas_added = 0
  63. for area in disallowed_areas:
  64. converted_points = []
  65. # Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise)
  66. clipped_area = area.intersectionConvexHulls(build_plate_polygon)
  67. for point in clipped_area.getPoints():
  68. converted_points.append(Point(point[0] * factor, point[1] * factor))
  69. disallowed_area = Item(converted_points)
  70. disallowed_area.markAsFixedInBin(0)
  71. node_items.append(disallowed_area)
  72. num_disallowed_areas_added += 1
  73. config = NfpConfig()
  74. config.accuracy = 1.0
  75. num_bins = nest(node_items, build_plate_bounding_box, 10000, config)
  76. # Strip the disallowed areas from the results again
  77. if num_disallowed_areas_added != 0:
  78. node_items = node_items[:-num_disallowed_areas_added]
  79. found_solution_for_all = num_bins == 1
  80. not_fit_count = 0
  81. grouped_operation = GroupedOperation()
  82. for node, node_item in zip(self._nodes, node_items):
  83. if node_item.binId() == 0:
  84. # We found a spot for it
  85. rotation_matrix = Matrix()
  86. rotation_matrix.setByRotationAxis(node_item.rotation(),Vector(0, -1, 0))
  87. grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix)))
  88. grouped_operation.addOperation(TranslateOperation(node, Vector(node_item.translation().x() / factor, 0, node_item.translation().y() / factor)))
  89. else:
  90. # We didn't find a spot
  91. grouped_operation.addOperation(TranslateOperation(node, Vector(200, 0, -not_fit_count * 20), set_position=True))
  92. not_fit_count += 1
  93. grouped_operation.push()
  94. status_message.hide()
  95. if not found_solution_for_all:
  96. no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"),
  97. title = i18n_catalog.i18nc("@info:title", "Can't Find Location"))
  98. no_full_solution_message.show()
  99. self.finished.emit(self)