From b0a8a5ccabe4c8fe3a85c5c755b4a7b235afe684 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 28 Sep 2020 16:16:47 +0200 Subject: [PATCH 01/28] Switch out the arranger algorithm for arrange all CURA-7440 --- cura/Arranging/ArrangeObjectsJob.py | 75 +++++++++-------------------- cura_app.py | 1 + 2 files changed, 24 insertions(+), 52 deletions(-) diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 387bf92688..38fbd0c3b4 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -4,6 +4,9 @@ from PyQt5.QtCore import QCoreApplication from UM.Application import Application from UM.Job import Job +from UM.Math.Matrix import Matrix +from UM.Math.Quaternion import Quaternion +from UM.Operations.RotateOperation import RotateOperation from UM.Scene.SceneNode import SceneNode from UM.Math.Vector import Vector from UM.Operations.TranslateOperation import TranslateOperation @@ -18,6 +21,7 @@ from cura.Arranging.Arrange import Arrange from cura.Arranging.ShapeArray import ShapeArray from typing import List +from pynest2d import * class ArrangeObjectsJob(Job): @@ -38,64 +42,31 @@ class ArrangeObjectsJob(Job): machine_width = global_container_stack.getProperty("machine_width", "value") machine_depth = global_container_stack.getProperty("machine_depth", "value") - arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes, min_offset = self._min_offset) + factor = 10000 - # Build set to exclude children (those get arranged together with the parents). - included_as_child = set() + build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor ) + + node_items = [] for node in self._nodes: - included_as_child.update(node.getAllChildren()) + hull_polygon = node.callDecoration("getConvexHull") + converted_points = [] + for point in hull_polygon.getPoints(): + converted_points.append(Point(point[0] * factor, point[1] * factor)) + item = Item(converted_points) + node_items.append(item) - # Collect nodes to be placed - nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) - for node in self._nodes: - if node in included_as_child: - continue - offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset, include_children = True) - if offset_shape_arr is None: - Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node)) - continue - nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr)) + config = NfpConfig() + config.accuracy = 1.0 + num_bins = nest(node_items, build_plate_bounding_box, 1, config) + found_solution_for_all = num_bins == 1 - # Sort the nodes with the biggest area first. - nodes_arr.sort(key=lambda item: item[0]) - nodes_arr.reverse() - - # Place nodes one at a time - start_priority = 0 - last_priority = start_priority - last_size = None grouped_operation = GroupedOperation() - found_solution_for_all = True - not_fit_count = 0 - for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr): - # For performance reasons, we assume that when a location does not fit, - # it will also not fit for the next object (while what can be untrue). - if last_size == size: # This optimization works if many of the objects have the same size - start_priority = last_priority - else: - start_priority = 0 - best_spot = arranger.bestSpot(hull_shape_arr, start_prio = start_priority) - x, y = best_spot.x, best_spot.y - node.removeDecorator(ZOffsetDecorator) - if node.getBoundingBox(): - center_y = node.getWorldPosition().y - node.getBoundingBox().bottom - else: - center_y = 0 - if x is not None: # We could find a place - last_size = size - last_priority = best_spot.priority + for node, node_item in zip(self._nodes, node_items): + rotation_matrix = Matrix() + rotation_matrix.setByRotationAxis(node_item.rotation(),Vector(0, -1, 0)) - arranger.place(x, y, offset_shape_arr) # take place before the next one - grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True)) - else: - Logger.log("d", "Arrange all: could not find spot!") - found_solution_for_all = False - grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, -not_fit_count * 20), set_position = True)) - not_fit_count += 1 - - status_message.setProgress((idx + 1) / len(nodes_arr) * 100) - Job.yieldThread() - QCoreApplication.processEvents() + 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.push() diff --git a/cura_app.py b/cura_app.py index 61fd544f8f..cc8a1d575c 100755 --- a/cura_app.py +++ b/cura_app.py @@ -22,6 +22,7 @@ import os # tries to create PyQt objects on a non-main thread. import Arcus # @UnusedImport import Savitar # @UnusedImport +import pynest2d # @UnusedImport from PyQt5.QtNetwork import QSslConfiguration, QSslSocket From 73289b2a7726070e71c210a4c755fffd9a038a7c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 28 Sep 2020 16:21:47 +0200 Subject: [PATCH 02/28] Place objects that we couldn't fit next to the buildplate CURA-7440 --- cura/Arranging/ArrangeObjectsJob.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 38fbd0c3b4..4ba648c495 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -59,15 +59,20 @@ class ArrangeObjectsJob(Job): config.accuracy = 1.0 num_bins = nest(node_items, build_plate_bounding_box, 1, config) found_solution_for_all = num_bins == 1 - + not_fit_count = 0 grouped_operation = GroupedOperation() for node, node_item in zip(self._nodes, node_items): - 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))) + if node_item.binId() == 0: + # We found a spot for it + 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))) + else: + # We didn't find a spot + grouped_operation.addOperation(TranslateOperation(node, Vector(200, 0, -not_fit_count * 20), set_position=True)) + not_fit_count += 1 grouped_operation.push() status_message.hide() From e0e65b91b53dbc961d3ed2ce1749bae1da4ef429 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 30 Sep 2020 10:34:35 +0200 Subject: [PATCH 03/28] Take disallowed areas into account when arranging CURA-7440 --- cura/Arranging/ArrangeObjectsJob.py | 48 +++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 4ba648c495..e26e1dfef3 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -1,10 +1,12 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import numpy from PyQt5.QtCore import QCoreApplication from UM.Application import Application from UM.Job import Job from UM.Math.Matrix import Matrix +from UM.Math.Polygon import Polygon from UM.Math.Quaternion import Quaternion from UM.Operations.RotateOperation import RotateOperation from UM.Scene.SceneNode import SceneNode @@ -41,11 +43,10 @@ class ArrangeObjectsJob(Job): global_container_stack = Application.getInstance().getGlobalContainerStack() machine_width = global_container_stack.getProperty("machine_width", "value") machine_depth = global_container_stack.getProperty("machine_depth", "value") - factor = 10000 + build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor) - build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor ) - + # Add all the items we want to arrange node_items = [] for node in self._nodes: hull_polygon = node.callDecoration("getConvexHull") @@ -55,9 +56,42 @@ class ArrangeObjectsJob(Job): 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)) + + build_volume = Application.getInstance().getBuildVolume() + 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) + + for point in clipped_area.getPoints(): + converted_points.append(Point(point[0] * factor, point[1] * factor)) + + disallowed_area = Item(converted_points) + disallowed_area.markAsFixedInBin(0) + node_items.append(disallowed_area) + num_disallowed_areas_added += 1 + config = NfpConfig() config.accuracy = 1.0 - num_bins = nest(node_items, build_plate_bounding_box, 1, config) + + num_bins = nest(node_items, build_plate_bounding_box, 10000, config) + + # Strip the disallowed areas from the results again + if num_disallowed_areas_added != 0: + node_items = node_items[:-num_disallowed_areas_added] + found_solution_for_all = num_bins == 1 not_fit_count = 0 grouped_operation = GroupedOperation() @@ -66,7 +100,6 @@ class ArrangeObjectsJob(Job): # We found a spot for it 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))) else: @@ -74,12 +107,9 @@ class ArrangeObjectsJob(Job): grouped_operation.addOperation(TranslateOperation(node, Vector(200, 0, -not_fit_count * 20), set_position=True)) not_fit_count += 1 grouped_operation.push() - status_message.hide() - if not found_solution_for_all: no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Can't Find Location")) no_full_solution_message.show() - - self.finished.emit(self) + self.finished.emit(self) \ No newline at end of file From c33c025a6c8c673b7ce84788feaf57a1176afed5 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 2 Oct 2020 10:04:46 +0200 Subject: [PATCH 04/28] Add getters for Width/height/depth to build volume CURA-7440 --- cura/BuildVolume.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 373f708389..ad1baf928e 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -180,12 +180,21 @@ class BuildVolume(SceneNode): def setWidth(self, width: float) -> None: self._width = width + def getWidth(self) -> float: + return self._width + def setHeight(self, height: float) -> None: self._height = height + def getHeight(self) -> float: + return self._height + def setDepth(self, depth: float) -> None: self._depth = depth + def getDepth(self) -> float: + return self._depth + def setShape(self, shape: str) -> None: if shape: self._shape = shape From 8c1985cdb3269dab6f0fdd5fdacd1d83b1b555cb Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 2 Oct 2020 10:05:45 +0200 Subject: [PATCH 05/28] Move nest2darrange to it's own function This makes it a lot easier to re-use it CURA-7440 --- cura/Arranging/ArrangeObjectsJob.py | 68 ++---------------------- cura/Arranging/Nest2DArrange.py | 81 +++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 65 deletions(-) create mode 100644 cura/Arranging/Nest2DArrange.py diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index e26e1dfef3..254cc4346b 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -16,6 +16,8 @@ from UM.Operations.GroupedOperation import GroupedOperation from UM.Logger import Logger from UM.Message import Message from UM.i18n import i18nCatalog +from cura.Arranging.Nest2DArrange import arrange + i18n_catalog = i18nCatalog("cura") from cura.Scene.ZOffsetDecorator import ZOffsetDecorator @@ -40,73 +42,9 @@ class ArrangeObjectsJob(Job): progress = 0, title = i18n_catalog.i18nc("@info:title", "Finding Location")) status_message.show() - global_container_stack = Application.getInstance().getGlobalContainerStack() - machine_width = global_container_stack.getProperty("machine_width", "value") - machine_depth = global_container_stack.getProperty("machine_depth", "value") - factor = 10000 - build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor) - # Add all the items we want to arrange - node_items = [] - for node in self._nodes: - hull_polygon = node.callDecoration("getConvexHull") - converted_points = [] - for point in hull_polygon.getPoints(): - converted_points.append(Point(point[0] * factor, point[1] * factor)) - item = Item(converted_points) - node_items.append(item) + found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume()) - # 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)) - - build_volume = Application.getInstance().getBuildVolume() - 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) - - for point in clipped_area.getPoints(): - converted_points.append(Point(point[0] * factor, point[1] * factor)) - - disallowed_area = Item(converted_points) - disallowed_area.markAsFixedInBin(0) - node_items.append(disallowed_area) - num_disallowed_areas_added += 1 - - config = NfpConfig() - config.accuracy = 1.0 - - num_bins = nest(node_items, build_plate_bounding_box, 10000, config) - - # Strip the disallowed areas from the results again - if num_disallowed_areas_added != 0: - node_items = node_items[:-num_disallowed_areas_added] - - found_solution_for_all = num_bins == 1 - not_fit_count = 0 - grouped_operation = GroupedOperation() - for node, node_item in zip(self._nodes, node_items): - if node_item.binId() == 0: - # We found a spot for it - 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))) - else: - # We didn't find a spot - grouped_operation.addOperation(TranslateOperation(node, Vector(200, 0, -not_fit_count * 20), set_position=True)) - not_fit_count += 1 - grouped_operation.push() status_message.hide() if not found_solution_for_all: no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py new file mode 100644 index 0000000000..55c6f0675a --- /dev/null +++ b/cura/Arranging/Nest2DArrange.py @@ -0,0 +1,81 @@ +import numpy +from pynest2d import * + +from UM.Math.Matrix import Matrix +from UM.Math.Polygon import Polygon +from UM.Math.Quaternion import Quaternion +from UM.Math.Vector import Vector +from UM.Operations.GroupedOperation import GroupedOperation +from UM.Operations.RotateOperation import RotateOperation +from UM.Operations.TranslateOperation import TranslateOperation + + +def arrange(nodes_to_arrange, build_volume, fixed_nodes = None, factor = 10000) -> bool: + machine_width = build_volume.getWidth() + machine_depth = build_volume.getDepth() + build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor) + + # Add all the items we want to arrange + node_items = [] + for node in nodes_to_arrange: + hull_polygon = node.callDecoration("getConvexHull") + converted_points = [] + for point in hull_polygon.getPoints(): + converted_points.append(Point(point[0] * factor, 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) + + for point in clipped_area.getPoints(): + converted_points.append(Point(point[0] * factor, point[1] * factor)) + + disallowed_area = Item(converted_points) + disallowed_area.markAsFixedInBin(0) + node_items.append(disallowed_area) + num_disallowed_areas_added += 1 + + config = NfpConfig() + config.accuracy = 1.0 + + num_bins = nest(node_items, build_plate_bounding_box, 10000, config) + + # Strip the disallowed areas from the results again + if num_disallowed_areas_added != 0: + node_items = node_items[:-num_disallowed_areas_added] + + found_solution_for_all = num_bins == 1 + not_fit_count = 0 + grouped_operation = GroupedOperation() + for node, node_item in zip(nodes_to_arrange, node_items): + if node_item.binId() == 0: + # We found a spot for it + 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))) + else: + # We didn't find a spot + grouped_operation.addOperation( + TranslateOperation(node, Vector(200, 0, -not_fit_count * 20), set_position=True)) + not_fit_count += 1 + grouped_operation.push() + + return found_solution_for_all \ No newline at end of file From c25351c98a8a888a6731a3e24ea77ef49861254b Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 2 Oct 2020 10:06:41 +0200 Subject: [PATCH 06/28] Remove unused imports CURA-7440 --- cura/Arranging/ArrangeObjectsJob.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 254cc4346b..82e979fd32 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -1,29 +1,14 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import numpy -from PyQt5.QtCore import QCoreApplication - from UM.Application import Application from UM.Job import Job -from UM.Math.Matrix import Matrix -from UM.Math.Polygon import Polygon -from UM.Math.Quaternion import Quaternion -from UM.Operations.RotateOperation import RotateOperation from UM.Scene.SceneNode import SceneNode -from UM.Math.Vector import Vector -from UM.Operations.TranslateOperation import TranslateOperation -from UM.Operations.GroupedOperation import GroupedOperation -from UM.Logger import Logger from UM.Message import Message from UM.i18n import i18nCatalog from cura.Arranging.Nest2DArrange import arrange i18n_catalog = i18nCatalog("cura") -from cura.Scene.ZOffsetDecorator import ZOffsetDecorator -from cura.Arranging.Arrange import Arrange -from cura.Arranging.ShapeArray import ShapeArray - from typing import List from pynest2d import * From 274dc8d06c6cdfaabe4eaef65913ded382dd34d9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 2 Oct 2020 11:21:57 +0200 Subject: [PATCH 07/28] Split up arrange This splits up the finding of new positions with the actual move. This is needed for when we want to place new nodes on model load. CURA-7440 --- cura/Arranging/Nest2DArrange.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index 55c6f0675a..523f292adb 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -10,7 +10,7 @@ from UM.Operations.RotateOperation import RotateOperation from UM.Operations.TranslateOperation import TranslateOperation -def arrange(nodes_to_arrange, build_volume, fixed_nodes = None, factor = 10000) -> bool: +def findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes = None, factor = 10000): machine_width = build_volume.getWidth() machine_depth = build_volume.getDepth() build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor) @@ -61,6 +61,13 @@ def arrange(nodes_to_arrange, build_volume, fixed_nodes = None, factor = 10000) node_items = node_items[:-num_disallowed_areas_added] found_solution_for_all = num_bins == 1 + + return found_solution_for_all, node_items + + +def arrange(nodes_to_arrange, build_volume, fixed_nodes = None, factor = 10000) -> bool: + found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor) + not_fit_count = 0 grouped_operation = GroupedOperation() for node, node_item in zip(nodes_to_arrange, node_items): @@ -78,4 +85,4 @@ def arrange(nodes_to_arrange, build_volume, fixed_nodes = None, factor = 10000) not_fit_count += 1 grouped_operation.push() - return found_solution_for_all \ No newline at end of file + return found_solution_for_all From 2d084c9e2b8819fc38ced05fa1db751a6138fe71 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 5 Oct 2020 10:22:39 +0200 Subject: [PATCH 08/28] Remove the old arrange from mesh placement on load For some reaso it was also handling the move up to the buildplate (which it really shouldn't do) CURA-7440 --- cura/CuraApplication.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 57f238a9d7..dd59955b80 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1815,17 +1815,13 @@ class CuraApplication(QtApplication): for node_ in DepthFirstIterator(root): if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate: fixed_nodes.append(node_) - machine_width = global_container_stack.getProperty("machine_width", "value") - machine_depth = global_container_stack.getProperty("machine_depth", "value") - arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes) - min_offset = 8 + default_extruder_position = self.getMachineManager().defaultExtruderPosition default_extruder_id = self._global_container_stack.extruderList[int(default_extruder_position)].getId() select_models_on_load = self.getPreferences().getValue("cura/select_models_on_load") for original_node in nodes: - # Create a CuraSceneNode just if the original node is not that type if isinstance(original_node, CuraSceneNode): node = original_node @@ -1833,8 +1829,8 @@ class CuraApplication(QtApplication): node = CuraSceneNode() node.setMeshData(original_node.getMeshData()) - #Setting meshdata does not apply scaling. - if(original_node.getScale() != Vector(1.0, 1.0, 1.0)): + # Setting meshdata does not apply scaling. + if original_node.getScale() != Vector(1.0, 1.0, 1.0): node.scale(original_node.getScale()) node.setSelectable(True) @@ -1865,19 +1861,13 @@ class CuraApplication(QtApplication): if file_extension != "3mf": if node.callDecoration("isSliceable"): - # Only check position if it's not already blatantly obvious that it won't fit. - if node.getBoundingBox() is None or self._volume.getBoundingBox() is None or node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: - # Find node location - offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset) + # Ensure that the bottom of the bounding box is on the build plate + if node.getBoundingBox(): + center_y = node.getWorldPosition().y - node.getBoundingBox().bottom + else: + center_y = 0 - # If a model is to small then it will not contain any points - if offset_shape_arr is None and hull_shape_arr is None: - Message(self._i18n_catalog.i18nc("@info:status", "The selected model was too small to load."), - title = self._i18n_catalog.i18nc("@info:title", "Warning")).show() - return - - # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher - arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10) + node.translate(Vector(0, center_y, 0)) # This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy # of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if From a34f1a6504e0f27d4dd54813482678c2a5b5bf7e Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 5 Oct 2020 13:14:21 +0200 Subject: [PATCH 09/28] Arrange newly placed models CURA-7440 --- cura/CuraApplication.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index dd59955b80..3ad15aaee8 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -53,6 +53,7 @@ from cura.Arranging.Arrange import Arrange from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob from cura.Arranging.ShapeArray import ShapeArray +from cura.Arranging.Nest2DArrange import findNodePlacement, arrange from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel @@ -1821,6 +1822,8 @@ class CuraApplication(QtApplication): select_models_on_load = self.getPreferences().getValue("cura/select_models_on_load") + nodes_to_arrange = [] # type: List[CuraSceneNode] + for original_node in nodes: # Create a CuraSceneNode just if the original node is not that type if isinstance(original_node, CuraSceneNode): @@ -1869,6 +1872,8 @@ class CuraApplication(QtApplication): node.translate(Vector(0, center_y, 0)) + nodes_to_arrange.append(node) + # This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy # of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if # the BuildPlateDecorator exists or not and always set the correct build plate number. @@ -1887,6 +1892,8 @@ class CuraApplication(QtApplication): if select_models_on_load: Selection.add(node) + arrange(nodes_to_arrange, self.getBuildVolume()) + self.fileCompleted.emit(file_name) def addNonSliceableExtension(self, extension): From 30966beed206c0a4a3eade7690c9175489e53114 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 5 Oct 2020 13:37:57 +0200 Subject: [PATCH 10/28] Let autoplacement also take the existing nodes into account CURA-7440 --- cura/Arranging/Nest2DArrange.py | 15 +++++++++++++++ cura/CuraApplication.py | 8 +++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index 523f292adb..84b69b2c2a 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -15,6 +15,9 @@ def findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes = None, factor machine_depth = build_volume.getDepth() build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor) + if fixed_nodes is None: + fixed_nodes = [] + # Add all the items we want to arrange node_items = [] for node in nodes_to_arrange: @@ -51,6 +54,18 @@ def findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes = None, factor node_items.append(disallowed_area) num_disallowed_areas_added += 1 + for node in fixed_nodes: + converted_points = [] + hull_polygon = node.callDecoration("getConvexHull") + + for point in hull_polygon.getPoints(): + converted_points.append(Point(point[0] * factor, point[1] * factor)) + item = Item(converted_points) + node_items.append(item) + item.markAsFixedInBin(0) + node_items.append(item) + num_disallowed_areas_added += 1 + config = NfpConfig() config.accuracy = 1.0 diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 3ad15aaee8..a6e37e6f09 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1823,6 +1823,12 @@ class CuraApplication(QtApplication): select_models_on_load = self.getPreferences().getValue("cura/select_models_on_load") nodes_to_arrange = [] # type: List[CuraSceneNode] + + fixed_nodes = [] + for node_ in DepthFirstIterator(self.getController().getScene().getRoot()): + # Only count sliceable objects + if node_.callDecoration("isSliceable"): + fixed_nodes.append(node_) for original_node in nodes: # Create a CuraSceneNode just if the original node is not that type @@ -1892,7 +1898,7 @@ class CuraApplication(QtApplication): if select_models_on_load: Selection.add(node) - arrange(nodes_to_arrange, self.getBuildVolume()) + arrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes) self.fileCompleted.emit(file_name) From 5823140b213548996e608eb186d36829ce840ff1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 5 Oct 2020 13:45:24 +0200 Subject: [PATCH 11/28] Always add multiplied nodes to the root. No idea why it was using the parent of the current node. It's technically the same, but this is much more explicit CURA-7440 --- cura/MultiplyObjectsJob.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 7507f2520e..533f631409 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -103,7 +103,7 @@ class MultiplyObjectsJob(Job): if nodes: operation = GroupedOperation() for new_node in nodes: - operation.addOperation(AddSceneNodeOperation(new_node, current_node.getParent())) + operation.addOperation(AddSceneNodeOperation(new_node, root)) operation.push() status_message.hide() From a1f43e7bcf07160bd23cde734479c018dcb4cfaa Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 5 Oct 2020 14:09:50 +0200 Subject: [PATCH 12/28] Also use the new arranger when multiplying objects --- cura/MultiplyObjectsJob.py | 64 +++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 533f631409..4ab0ab1e3f 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -7,10 +7,18 @@ from typing import List from PyQt5.QtCore import QCoreApplication from UM.Job import Job +from UM.Math.Matrix import Matrix +from UM.Math.Quaternion import Quaternion +from UM.Math.Vector import Vector from UM.Operations.GroupedOperation import GroupedOperation from UM.Message import Message +from UM.Operations.RotateOperation import RotateOperation +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 findNodePlacement + i18n_catalog = i18nCatalog("cura") from cura.Arranging.Arrange import Arrange @@ -43,11 +51,16 @@ class MultiplyObjectsJob(Job): machine_depth = global_container_stack.getProperty("machine_depth", "value") root = scene.getRoot() - scale = 0.5 - arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset) + processed_nodes = [] # type: List[SceneNode] nodes = [] + fixed_nodes = [] + for node_ in DepthFirstIterator(root): + # Only count sliceable objects + if node_.callDecoration("isSliceable"): + fixed_nodes.append(node_) + not_fit_count = 0 found_solution_for_all = False for node in self._objects: @@ -60,31 +73,8 @@ class MultiplyObjectsJob(Job): continue processed_nodes.append(current_node) - node_too_big = False - if node.getBoundingBox().width < machine_width or node.getBoundingBox().depth < machine_depth: - offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset = self._min_offset, scale = scale) - else: - node_too_big = True - - found_solution_for_all = True - arranger.resetLastPriority() for _ in range(self._count): - # We do place the nodes one by one, as we want to yield in between. new_node = copy.deepcopy(node) - solution_found = False - if not node_too_big: - if offset_shape_arr is not None and hull_shape_arr is not None: - solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr) - else: - # The node has no shape, so no need to arrange it. The solution is simple: Do nothing. - solution_found = True - - if node_too_big or not solution_found: - found_solution_for_all = False - new_location = new_node.getPosition() - new_location = new_location.set(z = - not_fit_count * 20) - new_node.setPosition(new_location) - not_fit_count += 1 # Same build plate build_plate_number = current_node.callDecoration("getBuildPlateNumber") @@ -93,17 +83,27 @@ class MultiplyObjectsJob(Job): child.callDecoration("setBuildPlateNumber", build_plate_number) nodes.append(new_node) - current_progress += 1 - status_message.setProgress((current_progress / total_progress) * 100) - QCoreApplication.processEvents() - Job.yieldThread() - QCoreApplication.processEvents() - Job.yieldThread() + factor = 10000 + found_solution_for_all, node_items = findNodePlacement(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, factor = 10000) if nodes: operation = GroupedOperation() - for new_node in nodes: + for new_node, node_item in zip(nodes, node_items): operation.addOperation(AddSceneNodeOperation(new_node, root)) + + if node_item.binId() == 0: + # We found a spot for it + rotation_matrix = Matrix() + rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0)) + operation.addOperation(RotateOperation(new_node, Quaternion.fromMatrix(rotation_matrix))) + operation.addOperation( + TranslateOperation(new_node, Vector(node_item.translation().x() / factor, 0, + node_item.translation().y() / factor))) + else: + # We didn't find a spot + operation.addOperation( + TranslateOperation(new_node, Vector(200, 0, -not_fit_count * 20), set_position=True)) + operation.push() status_message.hide() From 9312dc5c078bb43f276631dc14205de0aad98e30 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 5 Oct 2020 14:15:49 +0200 Subject: [PATCH 13/28] Add deprecated tag to arranger CURA-7440 --- cura/Arranging/Arrange.py | 4 ++++ cura/CuraApplication.py | 5 +++-- cura/MultiplyObjectsJob.py | 5 ----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cura/Arranging/Arrange.py b/cura/Arranging/Arrange.py index c9d3498c7b..efc8b8dd5c 100644 --- a/cura/Arranging/Arrange.py +++ b/cura/Arranging/Arrange.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. from typing import Optional +from UM.Decorators import deprecated from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Logger import Logger from UM.Math.Polygon import Polygon @@ -20,6 +21,7 @@ LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points """Return object for bestSpot""" +@deprecated("Use the functions in Nest2dArrange instead", "4.8") class Arrange: """ The Arrange classed is used together with :py:class:`cura.Arranging.ShapeArray.ShapeArray`. Use it to find good locations for objects that you try to put @@ -44,6 +46,7 @@ class Arrange: self._last_priority = 0 self._is_empty = True + @deprecated("Use the functions in Nest2dArrange instead", "4.8") @classmethod def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8) -> "Arrange": """Helper to create an :py:class:`cura.Arranging.Arrange.Arrange` instance @@ -101,6 +104,7 @@ class Arrange: self._last_priority = 0 + @deprecated("Use the functions in Nest2dArrange instead", "4.8") def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1) -> bool: """Find placement for a node (using offset shape) and place it (using hull shape) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index a6e37e6f09..a9555a6608 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -52,8 +52,7 @@ from cura.API.Account import Account from cura.Arranging.Arrange import Arrange from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob -from cura.Arranging.ShapeArray import ShapeArray -from cura.Arranging.Nest2DArrange import findNodePlacement, arrange +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 @@ -802,6 +801,8 @@ class CuraApplication(QtApplication): self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing build volume...")) root = self.getController().getScene().getRoot() self._volume = BuildVolume.BuildVolume(self, root) + + # Ensure that the old style arranger still works. Arrange.build_volume = self._volume # initialize info objects diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 4ab0ab1e3f..f35d95c2cb 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -4,8 +4,6 @@ import copy from typing import List -from PyQt5.QtCore import QCoreApplication - from UM.Job import Job from UM.Math.Matrix import Matrix from UM.Math.Quaternion import Quaternion @@ -21,9 +19,6 @@ from cura.Arranging.Nest2DArrange import findNodePlacement i18n_catalog = i18nCatalog("cura") -from cura.Arranging.Arrange import Arrange -from cura.Arranging.ShapeArray import ShapeArray - from UM.Application import Application from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation From a14135be5e9bddea66864f3de87c5ef36e738d50 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 5 Oct 2020 14:24:19 +0200 Subject: [PATCH 14/28] Add some documentation to the arrange nest algorithm CURA-7440 --- cura/Arranging/Nest2DArrange.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index 84b69b2c2a..d5be7f820e 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -1,5 +1,6 @@ import numpy from pynest2d import * +from typing import List, TYPE_CHECKING, Optional, Tuple from UM.Math.Matrix import Matrix from UM.Math.Polygon import Polygon @@ -10,7 +11,22 @@ from UM.Operations.RotateOperation import RotateOperation from UM.Operations.TranslateOperation import TranslateOperation -def findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes = None, factor = 10000): +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 = 10000) -> 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 accuracte we want it to be. + :return: + """ + machine_width = build_volume.getWidth() machine_depth = build_volume.getDepth() build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor) @@ -80,7 +96,17 @@ def findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes = None, factor return found_solution_for_all, node_items -def arrange(nodes_to_arrange, build_volume, fixed_nodes = None, factor = 10000) -> bool: +def arrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000) -> 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. + :return: + """ + found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor) not_fit_count = 0 From 59b9639d878d2e2acf45b61dffc1aa0f56b34e79 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 5 Oct 2020 14:31:42 +0200 Subject: [PATCH 15/28] Don't let arranger place all objects on the same spot if they don't fit CURA-7440 --- cura/MultiplyObjectsJob.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index f35d95c2cb..e560411852 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -36,14 +36,9 @@ class MultiplyObjectsJob(Job): status_message.show() scene = Application.getInstance().getController().getScene() - total_progress = len(self._objects) * self._count - current_progress = 0 - global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack is None: return # We can't do anything in this case. - machine_width = global_container_stack.getProperty("machine_width", "value") - machine_depth = global_container_stack.getProperty("machine_depth", "value") root = scene.getRoot() @@ -80,7 +75,7 @@ class MultiplyObjectsJob(Job): nodes.append(new_node) factor = 10000 found_solution_for_all, node_items = findNodePlacement(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, factor = 10000) - + not_fit_count = 0 if nodes: operation = GroupedOperation() for new_node, node_item in zip(nodes, node_items): @@ -98,6 +93,7 @@ class MultiplyObjectsJob(Job): # We didn't find a spot operation.addOperation( TranslateOperation(new_node, Vector(200, 0, -not_fit_count * 20), set_position=True)) + not_fit_count += 1 operation.push() status_message.hide() From d801f9838a5be2101ea4a3b1cb95ad767f24325d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 5 Oct 2020 14:52:50 +0200 Subject: [PATCH 16/28] Fix mypy issues It doesn't seem to like blanket imports CURA-7440 --- cura/Arranging/Nest2DArrange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index d5be7f820e..6774ebd982 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -1,5 +1,5 @@ import numpy -from pynest2d import * +from pynest2d import Point, Box, Item, NfpConfig, nest from typing import List, TYPE_CHECKING, Optional, Tuple from UM.Math.Matrix import Matrix From a46bf8d6fa7ec2602752c5a8e555b7d9f5c6cd23 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 8 Oct 2020 09:43:22 +0200 Subject: [PATCH 17/28] Apply suggestions from code review CURA-7440 Co-authored-by: Konstantinos Karmas --- cura/Arranging/ArrangeObjectsJob.py | 3 ++- cura/Arranging/Nest2DArrange.py | 2 +- cura/MultiplyObjectsJob.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 82e979fd32..da6e936d8d 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -35,4 +35,5 @@ class ArrangeObjectsJob(Job): no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Can't Find Location")) no_full_solution_message.show() - self.finished.emit(self) \ No newline at end of file + self.finished.emit(self) + diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index 6774ebd982..36d391e8a2 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -23,7 +23,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV :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 factor: The library that we use is int based. This factor defines how accurate we want it to be. :return: """ diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index e560411852..b54f7bcdce 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -74,7 +74,7 @@ class MultiplyObjectsJob(Job): nodes.append(new_node) factor = 10000 - found_solution_for_all, node_items = findNodePlacement(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, factor = 10000) + found_solution_for_all, node_items = findNodePlacement(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, factor = factor) not_fit_count = 0 if nodes: operation = GroupedOperation() From 6bfa24686fe5582874036932330b1a5f7c690f4a Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 8 Oct 2020 11:10:24 +0200 Subject: [PATCH 18/28] Mark disallowed areas as disallowed areas Also improve the filtering a bit so that it's no longer dependent on the order. Doesn't really matter in this case but it should be more robust. Contributes to issue CURA-7754. --- cura/Arranging/Nest2DArrange.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index 6774ebd982..ab0128d425 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -23,7 +23,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV :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 factor: The library that we use is int based. This factor defines how accurate we want it to be. :return: """ @@ -66,7 +66,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV converted_points.append(Point(point[0] * factor, point[1] * factor)) disallowed_area = Item(converted_points) - disallowed_area.markAsFixedInBin(0) + disallowed_area.markAsDisallowedAreaInBin(0) node_items.append(disallowed_area) num_disallowed_areas_added += 1 @@ -87,9 +87,8 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV num_bins = nest(node_items, build_plate_bounding_box, 10000, config) - # Strip the disallowed areas from the results again - if num_disallowed_areas_added != 0: - node_items = node_items[:-num_disallowed_areas_added] + # 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 From 8678682d943f0e9881560cbea1e140a7d3605299 Mon Sep 17 00:00:00 2001 From: Kostas Karmas Date: Thu, 8 Oct 2020 16:27:36 +0200 Subject: [PATCH 19/28] Reformat file and remove unused import CURA-7440 --- cura/Arranging/ArrangeObjectsJob.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index da6e936d8d..f84a0a8c42 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -1,17 +1,16 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import List + from UM.Application import Application from UM.Job import Job -from UM.Scene.SceneNode import SceneNode from UM.Message import Message +from UM.Scene.SceneNode import SceneNode from UM.i18n import i18nCatalog from cura.Arranging.Nest2DArrange import arrange i18n_catalog = i18nCatalog("cura") -from typing import List -from pynest2d import * - class ArrangeObjectsJob(Job): def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8) -> None: @@ -23,7 +22,7 @@ class ArrangeObjectsJob(Job): def run(self): status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"), lifetime = 0, - dismissable=False, + dismissable = False, progress = 0, title = i18n_catalog.i18nc("@info:title", "Finding Location")) status_message.show() @@ -32,8 +31,9 @@ class ArrangeObjectsJob(Job): status_message.hide() if not found_solution_for_all: - no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), - title = i18n_catalog.i18nc("@info:title", "Can't Find Location")) + no_full_solution_message = Message( + i18n_catalog.i18nc("@info:status", + "Unable to find a location within the build volume for all objects"), + title = i18n_catalog.i18nc("@info:title", "Can't Find Location")) no_full_solution_message.show() self.finished.emit(self) - From d3ec1d7d28bb260340cb654dd6960f66df4b1d82 Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Fri, 9 Oct 2020 13:39:53 +0200 Subject: [PATCH 20/28] Cache wasn't valid due to a reference being stored, not a copy. paer of CURA-7440 --- cura/Scene/ConvexHullDecorator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index a4228ce422..36697b7c57 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -269,7 +269,7 @@ class ConvexHullDecorator(SceneNodeDecorator): if mesh is None: return Polygon([]) # Node has no mesh data, so just return an empty Polygon. - world_transform = self._node.getWorldTransformation(copy = False) + world_transform = self._node.getWorldTransformation(copy = True) # Check the cache if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform: From eedbcb6a2d7fd1ed2cb1cb656a8e47ec380bfcbf Mon Sep 17 00:00:00 2001 From: Kostas Karmas Date: Fri, 9 Oct 2020 15:59:50 +0200 Subject: [PATCH 21/28] Fix z position of objects that end outside the buildplate When multiplying objects or inserting objects in the scene that do not fit in the buildplate, those objects would correctly end up outside the buildplate, but their Z position would be aligned at the absolute Z=0 of the buildplate. This commit fixes that by compensating in the z-axis, properly moving the object outside the buildplate in the correct z-position so that it's bottom is aligned with the original object. CURA-7440 --- cura/Arranging/Nest2DArrange.py | 3 ++- cura/MultiplyObjectsJob.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index 36d391e8a2..8e87228ded 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -112,6 +112,7 @@ def arrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fi not_fit_count = 0 grouped_operation = GroupedOperation() for node, node_item in zip(nodes_to_arrange, node_items): + if node_item.binId() == 0: # We found a spot for it rotation_matrix = Matrix() @@ -122,7 +123,7 @@ def arrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fi else: # We didn't find a spot grouped_operation.addOperation( - TranslateOperation(node, Vector(200, 0, -not_fit_count * 20), set_position=True)) + TranslateOperation(node, Vector(200, node.getWorldPosition().y, -not_fit_count * 20), set_position = True)) not_fit_count += 1 grouped_operation.push() diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index b54f7bcdce..24c74f655c 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -88,11 +88,10 @@ class MultiplyObjectsJob(Job): operation.addOperation(RotateOperation(new_node, Quaternion.fromMatrix(rotation_matrix))) operation.addOperation( TranslateOperation(new_node, Vector(node_item.translation().x() / factor, 0, - node_item.translation().y() / factor))) + node_item.translation().y() / factor))) else: # We didn't find a spot - operation.addOperation( - TranslateOperation(new_node, Vector(200, 0, -not_fit_count * 20), set_position=True)) + operation.addOperation(TranslateOperation(new_node, Vector(200, new_node.getWorldPosition().y, -not_fit_count * 20), set_position = True)) not_fit_count += 1 operation.push() From 518003f02705f57bb65fde0f96e0be6687e103da Mon Sep 17 00:00:00 2001 From: Kostas Karmas Date: Fri, 9 Oct 2020 16:03:31 +0200 Subject: [PATCH 22/28] Remove unused variables CURA-7440 --- cura/MultiplyObjectsJob.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 24c74f655c..25b3aad171 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -51,8 +51,6 @@ class MultiplyObjectsJob(Job): if node_.callDecoration("isSliceable"): fixed_nodes.append(node_) - not_fit_count = 0 - found_solution_for_all = False for node in self._objects: # If object is part of a group, multiply group current_node = node From f9bd5e3dcd223ae3a83223637f8d3994e2e3a1d3 Mon Sep 17 00:00:00 2001 From: Kostas Karmas Date: Fri, 9 Oct 2020 17:12:59 +0200 Subject: [PATCH 23/28] Modify arrange() to optionally create AddSceneNodeOperations Then the MultiplyObjectsJob can use the arrange function instead of duplicating the code CURA-7440 --- cura/Arranging/Nest2DArrange.py | 9 +++++-- cura/MultiplyObjectsJob.py | 46 +++++++++------------------------ 2 files changed, 19 insertions(+), 36 deletions(-) diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index 8e87228ded..b41c164147 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -2,10 +2,12 @@ import numpy from pynest2d import Point, Box, Item, NfpConfig, nest from typing import List, TYPE_CHECKING, Optional, Tuple +from UM.Application import Application from UM.Math.Matrix import Matrix from UM.Math.Polygon import Polygon from UM.Math.Quaternion import Quaternion from UM.Math.Vector import Vector +from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.RotateOperation import RotateOperation from UM.Operations.TranslateOperation import TranslateOperation @@ -96,7 +98,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV return found_solution_for_all, node_items -def arrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fixed_nodes: Optional[List["SceneNode"]] = None, factor = 10000) -> bool: +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) -> 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. @@ -104,14 +106,17 @@ def arrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fi :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 :return: """ - + scene_root = Application.getInstance().getController().getScene().getRoot() found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor) not_fit_count = 0 grouped_operation = GroupedOperation() for node, node_item in zip(nodes_to_arrange, node_items): + if add_new_nodes_in_scene: + grouped_operation.addOperation(AddSceneNodeOperation(node, scene_root)) if node_item.binId() == 0: # We found a spot for it diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 25b3aad171..1ba78edacf 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -4,24 +4,16 @@ import copy from typing import List +from UM.Application import Application from UM.Job import Job -from UM.Math.Matrix import Matrix -from UM.Math.Quaternion import Quaternion -from UM.Math.Vector import Vector -from UM.Operations.GroupedOperation import GroupedOperation from UM.Message import Message -from UM.Operations.RotateOperation import RotateOperation -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 findNodePlacement +from cura.Arranging.Nest2DArrange import arrange i18n_catalog = i18nCatalog("cura") -from UM.Application import Application -from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation - class MultiplyObjectsJob(Job): def __init__(self, objects, count, min_offset = 8): @@ -31,8 +23,9 @@ class MultiplyObjectsJob(Job): self._min_offset = min_offset def run(self) -> None: - status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0, - dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects")) + status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime = 0, + dismissable = False, progress = 0, + title = i18n_catalog.i18nc("@info:title", "Placing Objects")) status_message.show() scene = Application.getInstance().getController().getScene() @@ -71,30 +64,15 @@ class MultiplyObjectsJob(Job): child.callDecoration("setBuildPlateNumber", build_plate_number) nodes.append(new_node) - factor = 10000 - found_solution_for_all, node_items = findNodePlacement(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, factor = factor) - not_fit_count = 0 + + found_solution_for_all = True if nodes: - operation = GroupedOperation() - for new_node, node_item in zip(nodes, node_items): - operation.addOperation(AddSceneNodeOperation(new_node, root)) - - if node_item.binId() == 0: - # We found a spot for it - rotation_matrix = Matrix() - rotation_matrix.setByRotationAxis(node_item.rotation(), Vector(0, -1, 0)) - operation.addOperation(RotateOperation(new_node, Quaternion.fromMatrix(rotation_matrix))) - operation.addOperation( - TranslateOperation(new_node, Vector(node_item.translation().x() / factor, 0, - node_item.translation().y() / factor))) - else: - # We didn't find a spot - operation.addOperation(TranslateOperation(new_node, Vector(200, new_node.getWorldPosition().y, -not_fit_count * 20), set_position = True)) - not_fit_count += 1 - - operation.push() + found_solution_for_all = arrange(nodes, Application.getInstance().getBuildVolume(), fixed_nodes, + factor = 10000, add_new_nodes_in_scene = True) status_message.hide() if not found_solution_for_all: - no_full_solution_message = Message(i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), title = i18n_catalog.i18nc("@info:title", "Placing Object")) + no_full_solution_message = Message( + i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), + title = i18n_catalog.i18nc("@info:title", "Placing Object")) no_full_solution_message.show() From d47623d58aa9a81dc801aea0614344888206b0cf Mon Sep 17 00:00:00 2001 From: Kostas Karmas Date: Fri, 9 Oct 2020 17:19:53 +0200 Subject: [PATCH 24/28] Remove duplicate addition of items in the node_items list CURA-7440 --- cura/Arranging/Nest2DArrange.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index b41c164147..c66883f3a5 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -79,7 +79,6 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV for point in hull_polygon.getPoints(): converted_points.append(Point(point[0] * factor, point[1] * factor)) item = Item(converted_points) - node_items.append(item) item.markAsFixedInBin(0) node_items.append(item) num_disallowed_areas_added += 1 From a94ebc626101ba265e95327c7b26cca36530e2ce Mon Sep 17 00:00:00 2001 From: Kostas Karmas Date: Fri, 9 Oct 2020 18:09:40 +0200 Subject: [PATCH 25/28] Don't arrange locked objects when arrangeAll is called CURA-7440 --- cura/Arranging/ArrangeObjectsJob.py | 2 +- cura/CuraApplication.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index f84a0a8c42..481b8c2dc8 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -27,7 +27,7 @@ class ArrangeObjectsJob(Job): title = i18n_catalog.i18nc("@info:title", "Finding Location")) status_message.show() - found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume()) + found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes) status_message.hide() if not found_solution_for_all: diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index a9555a6608..ee6cc57523 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -36,6 +36,7 @@ from UM.Scene.Camera import Camera from UM.Scene.GroupDecorator import GroupDecorator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode +from UM.Scene.SceneNodeSettings import SceneNodeSettings from UM.Scene.Selection import Selection from UM.Scene.ToolHandle import ToolHandle from UM.Settings.ContainerRegistry import ContainerRegistry @@ -43,6 +44,7 @@ from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType from UM.Settings.SettingFunction import SettingFunction from UM.Settings.Validator import Validator +from UM.Util import parseBool from UM.View.SelectionPass import SelectionPass # For typing. from UM.Workspace.WorkspaceReader import WorkspaceReader from UM.i18n import i18nCatalog @@ -1381,6 +1383,7 @@ class CuraApplication(QtApplication): def arrangeAll(self) -> None: nodes_to_arrange = [] active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate + fixed_nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if not isinstance(node, SceneNode): continue @@ -1402,8 +1405,12 @@ class CuraApplication(QtApplication): # Skip nodes that are too big bounding_box = node.getBoundingBox() if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth: - nodes_to_arrange.append(node) - self.arrange(nodes_to_arrange, fixed_nodes = []) + # Arrange only the unlocked nodes and keep the locked ones in place + if not parseBool(node.getSetting(SceneNodeSettings.LockPosition)): + nodes_to_arrange.append(node) + else: + fixed_nodes.append(node) + self.arrange(nodes_to_arrange, fixed_nodes = fixed_nodes) def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None: """Arrange a set of nodes given a set of fixed nodes From 7e77910d10b8c3a34c3b010ec7f7617bf75d2b5c Mon Sep 17 00:00:00 2001 From: Kostas Karmas Date: Fri, 9 Oct 2020 18:17:10 +0200 Subject: [PATCH 26/28] Make the code of the locked_nodes more readable CURA-7440 --- cura/CuraApplication.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index ee6cc57523..08cc644025 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -44,7 +44,6 @@ from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType from UM.Settings.SettingFunction import SettingFunction from UM.Settings.Validator import Validator -from UM.Util import parseBool from UM.View.SelectionPass import SelectionPass # For typing. from UM.Workspace.WorkspaceReader import WorkspaceReader from UM.i18n import i18nCatalog @@ -1383,7 +1382,7 @@ class CuraApplication(QtApplication): def arrangeAll(self) -> None: nodes_to_arrange = [] active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate - fixed_nodes = [] + locked_nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if not isinstance(node, SceneNode): continue @@ -1406,11 +1405,11 @@ class CuraApplication(QtApplication): bounding_box = node.getBoundingBox() if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth: # Arrange only the unlocked nodes and keep the locked ones in place - if not parseBool(node.getSetting(SceneNodeSettings.LockPosition)): - nodes_to_arrange.append(node) + if UM.Util.parseBool(node.getSetting(SceneNodeSettings.LockPosition)): + locked_nodes.append(node) else: - fixed_nodes.append(node) - self.arrange(nodes_to_arrange, fixed_nodes = fixed_nodes) + nodes_to_arrange.append(node) + self.arrange(nodes_to_arrange, locked_nodes) def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None: """Arrange a set of nodes given a set of fixed nodes From 972e2f67164922fe283146ca150313c44d760d05 Mon Sep 17 00:00:00 2001 From: Kostas Karmas Date: Mon, 12 Oct 2020 16:28:28 +0200 Subject: [PATCH 27/28] Fix crash when arranging objects in one-at-a-time mode Crashing was occuring because node.getPoints() can return None. CURA-7440 --- cura/Arranging/Nest2DArrange.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index 1f75adaff0..f736e86592 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -64,24 +64,26 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV # Clip the disallowed areas so that they don't overlap the bounding box (The arranger chokes otherwise) clipped_area = area.intersectionConvexHulls(build_plate_polygon) - for point in clipped_area.getPoints(): - converted_points.append(Point(point[0] * factor, point[1] * factor)) + if clipped_area.getPoints() is not None: # numpy array has to be explicitly checked against None + for point in clipped_area.getPoints(): + converted_points.append(Point(point[0] * factor, point[1] * factor)) - disallowed_area = Item(converted_points) - disallowed_area.markAsDisallowedAreaInBin(0) - node_items.append(disallowed_area) - num_disallowed_areas_added += 1 + 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") - - for point in hull_polygon.getPoints(): - converted_points.append(Point(point[0] * factor, point[1] * factor)) - item = Item(converted_points) - item.markAsFixedInBin(0) - node_items.append(item) - num_disallowed_areas_added += 1 + + if hull_polygon.getPoints() is not None: # numpy array has to be explicitly checked against None + for point in hull_polygon.getPoints(): + converted_points.append(Point(point[0] * factor, point[1] * factor)) + item = Item(converted_points) + item.markAsFixedInBin(0) + node_items.append(item) + num_disallowed_areas_added += 1 config = NfpConfig() config.accuracy = 1.0 From a0e4e4325da361223ecfa658398c2eb5370e2bed Mon Sep 17 00:00:00 2001 From: Kostas Karmas Date: Mon, 12 Oct 2020 17:57:00 +0200 Subject: [PATCH 28/28] Add missing documentation CURA-7440 --- cura/Arranging/Nest2DArrange.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index f736e86592..93d0788970 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -26,7 +26,11 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV :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. - :return: + + :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 """ machine_width = build_volume.getWidth() @@ -107,7 +111,8 @@ def arrange(nodes_to_arrange: List["SceneNode"], build_volume: "BuildVolume", fi 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 - :return: + + :return: found_solution_for_all: Whether the algorithm found a place on the buildplate for all the objects """ scene_root = Application.getInstance().getController().getScene().getRoot() found_solution_for_all, node_items = findNodePlacement(nodes_to_arrange, build_volume, fixed_nodes, factor)