diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 0f8c576995..b1e0432786 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -9,7 +9,7 @@ from UM.Message import Message from UM.Scene.SceneNode import SceneNode from UM.i18n import i18nCatalog from cura.Arranging.GridArrange import GridArrange -from cura.Arranging.Nest2DArrange import arrange +from cura.Arranging.Nest2DArrange import Nest2DArrange i18n_catalog = i18nCatalog("cura") @@ -33,7 +33,13 @@ class ArrangeObjectsJob(Job): status_message.show() try: - found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes, grid_arrange= self._grid_arrange) + if self._grid_arrange: + arranger = GridArrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes) + else: + arranger = Nest2DArrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes, + factor=1000) + + found_solution_for_all = arranger.arrange() except: # If the thread crashes, the message should still close Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.") diff --git a/cura/Arranging/Arranger.py b/cura/Arranging/Arranger.py new file mode 100644 index 0000000000..684fa1258b --- /dev/null +++ b/cura/Arranging/Arranger.py @@ -0,0 +1,27 @@ +from typing import List, TYPE_CHECKING, Optional, Tuple, Set + +if TYPE_CHECKING: + from UM.Operations.GroupedOperation import GroupedOperation + + +class Arranger: + def createGroupOperationForArrange(self, add_new_nodes_in_scene: bool = True) -> Tuple["GroupedOperation", int]: + """ + Find placement for a set of scene nodes, but don't actually move them just yet. + :param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations + :return: tuple (found_solution_for_all, node_items) + WHERE + found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects + node_items: A list of the nodes return by libnest2d, which contain the new positions on the buildplate + """ + raise NotImplementedError + + def arrange(self, add_new_nodes_in_scene: bool = True) -> bool: + """ + Find placement for a set of scene nodes, and move them by using a single grouped operation. + :param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations + :return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects + """ + grouped_operation, not_fit_count = self.createGroupOperationForArrange(add_new_nodes_in_scene) + grouped_operation.push() + return not_fit_count == 0 diff --git a/cura/Arranging/GridArrange.py b/cura/Arranging/GridArrange.py index d8b7ba9db8..4866e99b69 100644 --- a/cura/Arranging/GridArrange.py +++ b/cura/Arranging/GridArrange.py @@ -1,8 +1,6 @@ import math from typing import List, TYPE_CHECKING, Optional, Tuple, Set - - if TYPE_CHECKING: from UM.Scene.SceneNode import SceneNode @@ -12,9 +10,10 @@ from UM.Math.Vector import Vector from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.TranslateOperation import TranslateOperation +from cura.Arranging.Arranger import Arranger -class GridArrange: +class GridArrange(Arranger): def __init__(self, nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: List["SceneNode"] = None): if fixed_nodes is None: fixed_nodes = [] @@ -38,7 +37,7 @@ class GridArrange: self._initial_leftover_grid_x = math.floor(self._initial_leftover_grid_x) self._initial_leftover_grid_y = math.floor(self._initial_leftover_grid_y) - def createGroupOperationForArrange(self) -> Tuple[GroupedOperation, int]: + def createGroupOperationForArrange(self, add_new_nodes_in_scene: bool = True) -> Tuple[GroupedOperation, int]: # Find grid indexes that intersect with fixed objects fixed_nodes_grid_ids = set() for node in self._fixed_nodes: @@ -77,7 +76,8 @@ class GridArrange: left_over_grid_y = self._initial_leftover_grid_y for node in leftover_nodes: - grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) + if add_new_nodes_in_scene: + grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) # find the first next grid position that isn't occupied by a fixed node while (self._initial_leftover_grid_x, left_over_grid_y) in fixed_nodes_grid_ids: left_over_grid_y = left_over_grid_y - 1 @@ -156,7 +156,7 @@ class GridArrange: # ┌───────┬───────┐ < coord_build_plate_back = -1 # │ │ │ # │ │(0,0) │ - # (-1,0)│───────o───────┤(1,0) + # (-1,0)├───────o───────┤(1,0) # │ │ │ # │ │ │ # └───────┴───────┘ < coord_build_plate_front = +1 diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index b7422e3943..ea45fe0b8e 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -15,131 +15,122 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.RotateOperation import RotateOperation from UM.Operations.TranslateOperation import TranslateOperation -from cura.Arranging.GridArrange import GridArrange +from cura.Arranging.Arranger import Arranger if TYPE_CHECKING: from UM.Scene.SceneNode import SceneNode from cura.BuildVolume import BuildVolume -def findNodePlacement( - nodes_to_arrange: List["SceneNode"], - build_volume: "BuildVolume", - fixed_nodes: Optional[List["SceneNode"]] = None, - factor: int = 10000, - *, - lock_rotation: bool = False -) -> Tuple[bool, List[Item]]: - """ - Find placement for a set of scene nodes, but don't actually move them just yet. - :param nodes_to_arrange: The list of nodes that need to be moved. - :param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this. - :param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes - are placed. - :param factor: The library that we use is int based. This factor defines how accurate we want it to be. - :param lock_rotation: If set to true the orientation of the object will remain the same +class Nest2DArrange(Arranger): + def __init__(self, + nodes_to_arrange: List["SceneNode"], + build_volume: "BuildVolume", + fixed_nodes: Optional[List["SceneNode"]] = None, + *, + factor: int = 10000, + lock_rotation: bool = False): + """ + :param nodes_to_arrange: The list of nodes that need to be moved. + :param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this. + :param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes + are placed. + :param factor: The library that we use is int based. This factor defines how accuracte we want it to be. + :param lock_rotation: If set to true the orientation of the object will remain the same + """ + super().__init__() + self._nodes_to_arrange = nodes_to_arrange + self._build_volume = build_volume + self._fixed_nodes = fixed_nodes + self._factor = factor + self._lock_rotation = lock_rotation - :return: tuple (found_solution_for_all, node_items) - WHERE - found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects - node_items: A list of the nodes return by libnest2d, which contain the new positions on the buildplate - """ - spacing = int(1.5 * factor) # 1.5mm spacing. + def findNodePlacement(self) -> Tuple[bool, List[Item]]: + spacing = int(1.5 * self._factor) # 1.5mm spacing. - machine_width = build_volume.getWidth() - machine_depth = build_volume.getDepth() - build_plate_bounding_box = Box(int(machine_width * factor), int(machine_depth * factor)) + machine_width = self._build_volume.getWidth() + machine_depth = self._build_volume.getDepth() + build_plate_bounding_box = Box(int(machine_width * self._factor), int(machine_depth * self._factor)) - if fixed_nodes is None: - fixed_nodes = [] + if self._fixed_nodes is None: + self._fixed_nodes = [] - # Add all the items we want to arrange - node_items = [] - for node in nodes_to_arrange: - hull_polygon = node.callDecoration("getConvexHull") - if not hull_polygon or hull_polygon.getPoints is None: - Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName())) - continue - converted_points = [] - for point in hull_polygon.getPoints(): - converted_points.append(Point(int(point[0] * factor), int(point[1] * factor))) - item = Item(converted_points) - node_items.append(item) - - # Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas) - half_machine_width = 0.5 * machine_width - 1 - half_machine_depth = 0.5 * machine_depth - 1 - build_plate_polygon = Polygon(numpy.array([ - [half_machine_width, -half_machine_depth], - [-half_machine_width, -half_machine_depth], - [-half_machine_width, half_machine_depth], - [half_machine_width, half_machine_depth] - ], numpy.float32)) - - disallowed_areas = build_volume.getDisallowedAreas() - num_disallowed_areas_added = 0 - for area in disallowed_areas: - converted_points = [] - - # Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise) - clipped_area = area.intersectionConvexHulls(build_plate_polygon) - - if clipped_area.getPoints() is not None and len(clipped_area.getPoints()) > 2: # numpy array has to be explicitly checked against None - for point in clipped_area.getPoints(): - converted_points.append(Point(int(point[0] * factor), int(point[1] * factor))) - - disallowed_area = Item(converted_points) - disallowed_area.markAsDisallowedAreaInBin(0) - node_items.append(disallowed_area) - num_disallowed_areas_added += 1 - - for node in fixed_nodes: - converted_points = [] - hull_polygon = node.callDecoration("getConvexHull") - - if hull_polygon is not None and hull_polygon.getPoints() is not None and len(hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None + # Add all the items we want to arrange + node_items = [] + for node in self._nodes_to_arrange: + hull_polygon = node.callDecoration("getConvexHull") + if not hull_polygon or hull_polygon.getPoints is None: + Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName())) + continue + converted_points = [] for point in hull_polygon.getPoints(): - converted_points.append(Point(int(point[0] * factor), int(point[1] * factor))) + converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor))) item = Item(converted_points) - item.markAsFixedInBin(0) node_items.append(item) - num_disallowed_areas_added += 1 - config = NfpConfig() - config.accuracy = 1.0 - config.alignment = NfpConfig.Alignment.DONT_ALIGN - if lock_rotation: - config.rotations = [0.0] + # Use a tiny margin for the build_plate_polygon (the nesting doesn't like overlapping disallowed areas) + half_machine_width = 0.5 * machine_width - 1 + half_machine_depth = 0.5 * machine_depth - 1 + build_plate_polygon = Polygon(numpy.array([ + [half_machine_width, -half_machine_depth], + [-half_machine_width, -half_machine_depth], + [-half_machine_width, half_machine_depth], + [half_machine_width, half_machine_depth] + ], numpy.float32)) - num_bins = nest(node_items, build_plate_bounding_box, spacing, config) + disallowed_areas = self._build_volume.getDisallowedAreas() + num_disallowed_areas_added = 0 + for area in disallowed_areas: + converted_points = [] - # Strip the fixed items (previously placed) and the disallowed areas from the results again. - node_items = list(filter(lambda item: not item.isFixed(), node_items)) + # Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise) + clipped_area = area.intersectionConvexHulls(build_plate_polygon) - found_solution_for_all = num_bins == 1 + if clipped_area.getPoints() is not None and len( + clipped_area.getPoints()) > 2: # numpy array has to be explicitly checked against None + for point in clipped_area.getPoints(): + converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor))) - return found_solution_for_all, node_items + disallowed_area = Item(converted_points) + disallowed_area.markAsDisallowedAreaInBin(0) + node_items.append(disallowed_area) + num_disallowed_areas_added += 1 + for node in self._fixed_nodes: + converted_points = [] + hull_polygon = node.callDecoration("getConvexHull") -def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"], - build_volume: "BuildVolume", - fixed_nodes: Optional[List["SceneNode"]] = None, - factor: int = 10000, - *, - add_new_nodes_in_scene: bool = False, - lock_rotation: bool = False, - grid_arrange: bool = False) -> Tuple[GroupedOperation, int]: - if grid_arrange: - grid = GridArrange(nodes_to_arrange, build_volume, fixed_nodes) - return grid.createGroupOperationForArrange() - else: + if hull_polygon is not None and hull_polygon.getPoints() is not None and len( + hull_polygon.getPoints()) > 2: # numpy array has to be explicitly checked against None + for point in hull_polygon.getPoints(): + converted_points.append(Point(int(point[0] * self._factor), int(point[1] * self._factor))) + item = Item(converted_points) + item.markAsFixedInBin(0) + node_items.append(item) + num_disallowed_areas_added += 1 + + config = NfpConfig() + config.accuracy = 1.0 + config.alignment = NfpConfig.Alignment.DONT_ALIGN + if self._lock_rotation: + config.rotations = [0.0] + + num_bins = nest(node_items, build_plate_bounding_box, spacing, config) + + # Strip the fixed items (previously placed) and the disallowed areas from the results again. + node_items = list(filter(lambda item: not item.isFixed(), node_items)) + + found_solution_for_all = num_bins == 1 + + return found_solution_for_all, node_items + + def createGroupOperationForArrange(self, add_new_nodes_in_scene: bool = True) -> Tuple[GroupedOperation, int]: scene_root = Application.getInstance().getController().getScene().getRoot() - found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor, - lock_rotation = lock_rotation) + found_solution_for_all, node_items = self.findNodePlacement() not_fit_count = 0 grouped_operation = GroupedOperation() - for node, node_item in zip(nodes_to_arrange, node_items): + for node, node_item in zip(self._nodes_to_arrange, node_items): if add_new_nodes_in_scene: grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) @@ -148,40 +139,13 @@ def createGroupOperationForArrange(nodes_to_arrange: List["SceneNode"], rotation_matrix = Matrix() rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0)) grouped_operation.addOperation(RotateOperation(node, Quaternion.fromMatrix(rotation_matrix))) - grouped_operation.addOperation(TranslateOperation(node, Vector(node_item.translation().x() / factor, 0, - node_item.translation().y() / factor))) + grouped_operation.addOperation( + TranslateOperation(node, Vector(node_item.translation().x() / self._factor, 0, + node_item.translation().y() / self._factor))) else: # We didn't find a spot grouped_operation.addOperation( TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True)) not_fit_count += 1 - return grouped_operation, not_fit_count - - -def arrange( - nodes_to_arrange: List["SceneNode"], - build_volume: "BuildVolume", - fixed_nodes: Optional[List["SceneNode"]] = None, - factor=10000, - add_new_nodes_in_scene: bool = False, - lock_rotation: bool = False, - grid_arrange: bool = False -) -> bool: - """ - Find placement for a set of scene nodes, and move them by using a single grouped operation. - :param nodes_to_arrange: The list of nodes that need to be moved. - :param build_volume: The build volume that we want to place the nodes in. It gets size & disallowed areas from this. - :param fixed_nodes: List of nods that should not be moved, but should be used when deciding where the others nodes - are placed. - :param factor: The library that we use is int based. This factor defines how accuracte we want it to be. - :param add_new_nodes_in_scene: Whether to create new scene nodes before applying the transformations and rotations - :param lock_rotation: If set to true the orientation of the object will remain the same - - :return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects - """ - - grouped_operation, not_fit_count = createGroupOperationForArrange(nodes_to_arrange, build_volume, fixed_nodes, - factor, add_new_nodes_in_scene = add_new_nodes_in_scene, lock_rotation = lock_rotation, grid_arrange = grid_arrange) - grouped_operation.push() - return not_fit_count == 0 + return grouped_operation, not_fit_count diff --git a/cura/CuraActions.py b/cura/CuraActions.py index dc18497191..29f50d88b2 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -22,7 +22,10 @@ from cura.Operations.SetParentOperation import SetParentOperation from cura.MultiplyObjectsJob import MultiplyObjectsJob from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation from cura.Settings.ExtruderManager import ExtruderManager -from cura.Arranging.Nest2DArrange import createGroupOperationForArrange + +from cura.Arranging.GridArrange import GridArrange +from cura.Arranging.Nest2DArrange import Nest2DArrange + from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation @@ -238,7 +241,9 @@ class CuraActions(QObject): if node.callDecoration("isSliceable"): fixed_nodes.append(node) # Add the new nodes to the scene, and arrange them - group_operation, not_fit_count = createGroupOperationForArrange(nodes, application.getBuildVolume(), fixed_nodes, grid_arrange = True) + + arranger = GridArrange(nodes, application.getBuildVolume(), fixed_nodes) + group_operation, not_fit_count = arranger.createGroupOperationForArrange() group_operation.push() # deselect currently selected nodes, and select the new nodes diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index fb4b4cc5d6..54b717b3d0 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -54,7 +54,6 @@ from cura import ApplicationMetadata from cura.API import CuraAPI from cura.API.Account import Account from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob -from cura.Arranging.Nest2DArrange import arrange from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 3864b94427..889b6f5d1a 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -14,7 +14,8 @@ from UM.Operations.TranslateOperation import TranslateOperation from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode from UM.i18n import i18nCatalog -from cura.Arranging.Nest2DArrange import createGroupOperationForArrange +from cura.Arranging.GridArrange import GridArrange +from cura.Arranging.Nest2DArrange import Nest2DArrange i18n_catalog = i18nCatalog("cura") @@ -77,12 +78,12 @@ class MultiplyObjectsJob(Job): found_solution_for_all = True group_operation = GroupedOperation() if nodes: - group_operation, not_fit_count = createGroupOperationForArrange(nodes, - Application.getInstance().getBuildVolume(), - fixed_nodes, - factor=10000, - add_new_nodes_in_scene=True, - grid_arrange=self._grid_arrange) + if self._grid_arrange: + arranger = GridArrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes) + else: + arranger = Nest2DArrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, factor=1000) + + group_operation, not_fit_count = arranger.createGroupOperationForArrange(add_new_nodes_in_scene=True) if nodes_to_add_without_arrange: for nested_node in nodes_to_add_without_arrange: