Nest2DArrange.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. # Copyright (c) 2020 Ultimaker B.V.
  2. # Cura is released under the terms of the LGPLv3 or higher.
  3. import numpy
  4. from pynest2d import Point, Box, Item, NfpConfig, nest
  5. from typing import List, TYPE_CHECKING, Optional, Tuple
  6. from UM.Application import Application
  7. from UM.Decorators import deprecated
  8. from UM.Logger import Logger
  9. from UM.Math.Matrix import Matrix
  10. from UM.Math.Polygon import Polygon
  11. from UM.Math.Quaternion import Quaternion
  12. from UM.Math.Vector import Vector
  13. from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
  14. from UM.Operations.GroupedOperation import GroupedOperation
  15. from UM.Operations.RotateOperation import RotateOperation
  16. from UM.Operations.TranslateOperation import TranslateOperation
  17. from cura.Arranging.Arranger import Arranger
  18. if TYPE_CHECKING:
  19. from UM.Scene.SceneNode import SceneNode
  20. from cura.BuildVolume import BuildVolume
  21. class Nest2DArrange(Arranger):
  22. def __init__(self,
  23. nodes_to_arrange: List["SceneNode"],
  24. build_volume: "BuildVolume",
  25. fixed_nodes: Optional[List["SceneNode"]] = None,
  26. *,
  27. factor: int = 10000,
  28. lock_rotation: bool = False):
  29. """
  30. :param nodes_to_arrange: The list of nodes that need to be moved.
  31. :param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this.
  32. :param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes
  33. are placed.
  34. :param factor: The library that we use is int based. This factor defines how accuracte we want it to be.
  35. :param lock_rotation: If set to true the orientation of the object will remain the same
  36. """
  37. super().__init__()
  38. self._nodes_to_arrange = nodes_to_arrange
  39. self._build_volume = build_volume
  40. self._fixed_nodes = fixed_nodes
  41. self._factor = factor
  42. self._lock_rotation = lock_rotation
  43. def findNodePlacement(self) -> Tuple[bool, List[Item]]:
  44. spacing = int(1.5 * self._factor) # 1.5mm spacing.
  45. edge_disallowed_size = self._build_volume.getEdgeDisallowedSize()
  46. machine_width = self._build_volume.getWidth() - (edge_disallowed_size * 2)
  47. machine_depth = self._build_volume.getDepth() - (edge_disallowed_size * 2)
  48. build_plate_bounding_box = Box(int(machine_width * self._factor), int(machine_depth * self._factor))
  49. if self._fixed_nodes is None:
  50. self._fixed_nodes = []
  51. # Add all the items we want to arrange
  52. node_items = []
  53. for node in self._nodes_to_arrange:
  54. hull_polygon = node.callDecoration("getConvexHull")
  55. if not hull_polygon or hull_polygon.getPoints is None:
  56. Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName()))
  57. continue
  58. converted_points = []
  59. for point in hull_polygon.getPoints():
  60. converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
  61. item = Item(converted_points)
  62. node_items.append(item)
  63. # Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas)
  64. half_machine_width = 0.5 * machine_width - 1
  65. half_machine_depth = 0.5 * machine_depth - 1
  66. build_plate_polygon = Polygon(numpy.array([
  67. [half_machine_width, -half_machine_depth],
  68. [-half_machine_width, -half_machine_depth],
  69. [-half_machine_width, half_machine_depth],
  70. [half_machine_width, half_machine_depth]
  71. ], numpy.float32))
  72. disallowed_areas = self._build_volume.getDisallowedAreas()
  73. for area in disallowed_areas:
  74. converted_points = []
  75. # Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise)
  76. clipped_area = area.intersectionConvexHulls(build_plate_polygon)
  77. if clipped_area.getPoints() is not None and len(
  78. clipped_area.getPoints()) > 2: # numpy array has to be explicitly checked against None
  79. for point in clipped_area.getPoints():
  80. converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
  81. disallowed_area = Item(converted_points)
  82. disallowed_area.markAsDisallowedAreaInBin(0)
  83. node_items.append(disallowed_area)
  84. for node in self._fixed_nodes:
  85. converted_points = []
  86. hull_polygon = node.callDecoration("getConvexHull")
  87. if hull_polygon is not None and hull_polygon.getPoints() is not None and len(
  88. hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None
  89. for point in hull_polygon.getPoints():
  90. converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor)))
  91. item = Item(converted_points)
  92. item.markAsFixedInBin(0)
  93. node_items.append(item)
  94. strategies = [NfpConfig.Alignment.CENTER] * 3 + [NfpConfig.Alignment.BOTTOM_LEFT] * 3
  95. found_solution_for_all = False
  96. while not found_solution_for_all and len(strategies) > 0:
  97. config = NfpConfig()
  98. config.accuracy = 1.0
  99. config.alignment = NfpConfig.Alignment.CENTER
  100. config.starting_point = strategies[0]
  101. strategies = strategies[1:]
  102. if self._lock_rotation:
  103. config.rotations = [0.0]
  104. num_bins = nest(node_items, build_plate_bounding_box, spacing, config)
  105. # Strip the fixed items (previously placed) and the disallowed areas from the results again.
  106. node_items = list(filter(lambda item: not item.isFixed(), node_items))
  107. found_solution_for_all = num_bins == 1
  108. return found_solution_for_all, node_items
  109. def createGroupOperationForArrange(self, add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]:
  110. scene_root = Application.getInstance().getController().getScene().getRoot()
  111. found_solution_for_all, node_items = self.findNodePlacement()
  112. not_fit_count = 0
  113. grouped_operation = GroupedOperation()
  114. for node, node_item in zip(self._nodes_to_arrange, node_items):
  115. if add_new_nodes_in_scene:
  116. grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root))
  117. if node_item.binId() == 0:
  118. # We found a spot for it
  119. rotation_matrix = Matrix()
  120. rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0))
  121. grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix)))
  122. grouped_operation.addOperation(
  123. TranslateOperation(node, Vector(node_item.translation().x() / self._factor, 0,
  124. node_item.translation().y() / self._factor)))
  125. else:
  126. # We didn't find a spot
  127. grouped_operation.addOperation(
  128. TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True))
  129. not_fit_count += 1
  130. return grouped_operation, not_fit_count
  131. @deprecated("Use the Nest2DArrange class instead")
  132. def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume",
  133. fixed_nodes: Optional[List["SceneNode"]] = None, factor=10000) -> Tuple[bool, List[Item]]:
  134. arranger = Nest2DArrange(nodes_to_arrange, build_volume, fixed_nodes, factor=factor)
  135. return arranger.findNodePlacement()
  136. @deprecated("Use the Nest2DArrange class instead")
  137. def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"],
  138. build_volume: "BuildVolume",
  139. fixed_nodes: Optional[List["SceneNode"]] = None,
  140. factor=10000,
  141. add_new_nodes_in_scene: bool = False) -> Tuple[GroupedOperation, int]:
  142. arranger = Nest2DArrange(nodes_to_arrange, build_volume, fixed_nodes, factor=factor)
  143. return arranger.createGroupOperationForArrange(add_new_nodes_in_scene=add_new_nodes_in_scene)
  144. @deprecated("Use the Nest2DArrange class instead")
  145. def arrange(nodes_to_arrange: List["SceneNode"],
  146. build_volume: "BuildVolume",
  147. fixed_nodes: Optional[List["SceneNode"]] = None,
  148. factor=10000,
  149. add_new_nodes_in_scene: bool = False) -> bool:
  150. arranger = Nest2DArrange(nodes_to_arrange, build_volume, fixed_nodes, factor=factor)
  151. return arranger.arrange(add_new_nodes_in_scene=add_new_nodes_in_scene)