diff --git a/cura/Arrange.py b/cura/Arrange.py index 0d1f2e0c06..305729d763 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -30,6 +30,7 @@ class Arrange: self._offset_x = offset_x self._offset_y = offset_y self._last_priority = 0 + self._is_empty = True ## Helper to create an Arranger instance # @@ -38,8 +39,8 @@ class Arrange: # \param scene_root Root for finding all scene nodes # \param fixed_nodes Scene nodes to be placed @classmethod - def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5): - arranger = Arrange(220, 220, 110, 110, scale = scale) + def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 220, y = 220): + arranger = Arrange(x, y, x / 2, y / 2, scale = scale) arranger.centerFirst() if fixed_nodes is None: @@ -62,7 +63,7 @@ class Arrange: for area in disallowed_areas: points = copy.deepcopy(area._points) shape_arr = ShapeArray.fromPolygon(points, scale = scale) - arranger.place(0, 0, shape_arr) + arranger.place(0, 0, shape_arr, update_empty = False) return arranger ## Find placement for a node (using offset shape) and place it (using hull shape) @@ -166,7 +167,7 @@ class Arrange: # \param x x-coordinate # \param y y-coordinate # \param shape_arr ShapeArray object - def place(self, x, y, shape_arr): + def place(self, x, y, shape_arr, update_empty = True): x = int(self._scale * x) y = int(self._scale * y) offset_x = x + self._offset_x + shape_arr.offset_x @@ -179,10 +180,17 @@ class Arrange: max_y = min(max(offset_y + shape_arr.arr.shape[0], 0), shape_y - 1) occupied_slice = self._occupied[min_y:max_y, min_x:max_x] # we use a slice of shape because it can be out of bounds - occupied_slice[numpy.where(shape_arr.arr[ - min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 1 + new_occupied = numpy.where(shape_arr.arr[ + min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1) + if update_empty and new_occupied: + self._is_empty = False + occupied_slice[new_occupied] = 1 # Set priority to low (= high number), so it won't get picked at trying out. prio_slice = self._priority[min_y:max_y, min_x:max_x] prio_slice[numpy.where(shape_arr.arr[ min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999 + + @property + def isEmpty(self): + return self._is_empty diff --git a/cura/ArrangeObjectsAllBuildPlatesJob.py b/cura/ArrangeObjectsAllBuildPlatesJob.py new file mode 100644 index 0000000000..eacd18d5ad --- /dev/null +++ b/cura/ArrangeObjectsAllBuildPlatesJob.py @@ -0,0 +1,159 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from UM.Job import Job +from UM.Scene.SceneNode import SceneNode +from UM.Math.Vector import Vector +from UM.Operations.SetTransformOperation import SetTransformOperation +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 +i18n_catalog = i18nCatalog("cura") + +from cura.ZOffsetDecorator import ZOffsetDecorator +from cura.Arrange import Arrange +from cura.ShapeArray import ShapeArray + +from typing import List + + +class ArrangeArray: + def __init__(self, x, y, fixed_nodes): + self._x = x + self._y = y + self._fixed_nodes = fixed_nodes + self._count = 0 + self._first_empty = None + self._has_empty = False + self._arrange = [] + + def _update_first_empty(self): + for i, a in enumerate(self._arrange): + if a.isEmpty: + self._first_empty = i + self._has_empty = True + + Logger.log("d", "lala %s %s", self._first_empty, self._has_empty) + return + self._first_empty = None + self._has_empty = False + + def add(self): + new_arrange = Arrange.create(x = self._x, y = self._y, fixed_nodes = self._fixed_nodes) + self._arrange.append(new_arrange) + self._count += 1 + self._update_first_empty() + + def count(self): + return self._count + + def get(self, index): + return self._arrange[index] + + def getFirstEmpty(self): + if not self._is_empty: + self.add() + return self._arrange[self._first_empty] + + +class ArrangeObjectsAllBuildPlatesJob(Job): + def __init__(self, nodes: List[SceneNode], min_offset = 8): + super().__init__() + self._nodes = nodes + self._min_offset = min_offset + + def run(self): + status_message = Message(i18n_catalog.i18nc("@info:status", "Finding new location for objects"), + lifetime = 0, + dismissable=False, + progress = 0, + title = i18n_catalog.i18nc("@info:title", "Finding Location")) + status_message.show() + + + # Collect nodes to be placed + nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) + for node in self._nodes: + offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset) + nodes_arr.append((offset_shape_arr.arr.shape[0] * offset_shape_arr.arr.shape[1], node, offset_shape_arr, hull_shape_arr)) + + # Sort the nodes with the biggest area first. + nodes_arr.sort(key=lambda item: item[0]) + nodes_arr.reverse() + + x, y = 200, 200 + + arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = []) + arrange_array.add() + + # Place nodes one at a time + start_priority = 0 + grouped_operation = GroupedOperation() + found_solution_for_all = True + left_over_nodes = [] # nodes that do not fit on an empty build plate + + 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). + # We also skip possibilities by slicing through the possibilities (step = 10) + + try_placement = True + + current_build_plate_number = 0 # always start with the first one + + # # Only for first build plate + # if last_size == size and last_build_plate_number == current_build_plate_number: + # # This optimization works if many of the objects have the same size + # # Continue with same build plate number + # start_priority = last_priority + # else: + # start_priority = 0 + + while try_placement: + Logger.log("d", "start_priority %s", start_priority) + # make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects + while current_build_plate_number >= arrange_array.count(): + arrange_array.add() + arranger = arrange_array.get(current_build_plate_number) + + best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10) + 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 + arranger.place(x, y, hull_shape_arr) # place the object in the arranger + + node.callDecoration("setBuildPlateNumber", current_build_plate_number) + grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True)) + try_placement = False + else: + # very naive, because we skip to the next build plate if one model doesn't fit. + if arranger.isEmpty: + # apparently we can never place this object + left_over_nodes.append(node) + try_placement = False + else: + # try next build plate + current_build_plate_number += 1 + try_placement = True + + status_message.setProgress((idx + 1) / len(nodes_arr) * 100) + Job.yieldThread() + + for node in left_over_nodes: + node.callDecoration("setBuildPlateNumber", -1) # these are not on any build plate + found_solution_for_all = False + + 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() diff --git a/cura/CuraActions.py b/cura/CuraActions.py index c313488cac..dbcd31f646 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -134,25 +134,16 @@ class CuraActions(QObject): Logger.log("d", "Setting build plate number... %d" % build_plate_nr) operation = GroupedOperation() + root = Application.getInstance().getController().getScene().getRoot() + nodes_to_change = [] for node in Selection.getAllSelectedObjects(): - # Do not change any nodes that already have the right extruder set. - if node.callDecoration("getBuildPlateNumber") == build_plate_nr: - continue + parent_node = node # Find the parent node to change instead + while parent_node.getParent() != root: + parent_node = parent_node.getParent() - # If the node is a group, apply the active extruder to all children of the group. - if node.callDecoration("isGroup"): - for grouped_node in BreadthFirstIterator(node): - if grouped_node.callDecoration("getBuildPlateNumber") == build_plate_nr: - continue - - if grouped_node.callDecoration("isGroup"): - continue - - nodes_to_change.append(grouped_node) - continue - - nodes_to_change.append(node) + for single_node in BreadthFirstIterator(parent_node): + nodes_to_change.append(single_node) if not nodes_to_change: Logger.log("d", "Nothing to change.") diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 7a6994e83e..39e4b38824 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -39,8 +39,11 @@ from cura.ConvexHullDecorator import ConvexHullDecorator from cura.SetParentOperation import SetParentOperation from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.BlockSlicingDecorator import BlockSlicingDecorator +# research +from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.ArrangeObjectsJob import ArrangeObjectsJob +from cura.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob from cura.MultiplyObjectsJob import MultiplyObjectsJob from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType @@ -54,8 +57,6 @@ from cura.Settings.SettingInheritanceManager import SettingInheritanceManager from cura.Settings.UserProfilesModel import UserProfilesModel from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager -# research -from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from . import PlatformPhysics from . import BuildVolume @@ -1105,7 +1106,7 @@ class CuraApplication(QtApplication): ## Arrange all objects. @pyqtSlot() - def arrangeAll(self): + def arrangeObjectsToAllBuildPlates(self): nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if type(node) is not SceneNode: @@ -1119,6 +1120,26 @@ class CuraApplication(QtApplication): # Skip nodes that are too big if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: nodes.append(node) + job = ArrangeObjectsAllBuildPlatesJob(nodes) + job.start() + + # Single build plate + @pyqtSlot() + def arrangeAll(self): + nodes = [] + for node in DepthFirstIterator(self.getController().getScene().getRoot()): + if type(node) is not SceneNode: + continue + if not node.getMeshData() and not node.callDecoration("isGroup"): + continue # Node that doesnt have a mesh and is not a group. + if node.getParent() and node.getParent().callDecoration("isGroup"): + continue # Grouped nodes don't need resetting as their parent (the group) is resetted) + if not node.isSelectable(): + continue # i.e. node with layer data + if node.callDecoration("getBuildPlateNumber") == self._active_build_plate: + # Skip nodes that are too big + if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: + nodes.append(node) self.arrange(nodes, fixed_nodes = []) ## Arrange Selection @@ -1250,6 +1271,7 @@ class CuraApplication(QtApplication): group_decorator = GroupDecorator() group_node.addDecorator(group_decorator) group_node.addDecorator(ConvexHullDecorator()) + group_node.addDecorator(BuildPlateDecorator(self.activeBuildPlate)) group_node.setParent(self.getController().getScene().getRoot()) group_node.setSelectable(True) center = Selection.getSelectionCenter() diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 721c0e4c07..63a38993a2 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -13,6 +13,7 @@ from UM.i18n import i18nCatalog i18n_catalog = i18nCatalog("cura") from cura.ZOffsetDecorator import ZOffsetDecorator +from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.Arrange import Arrange from cura.ShapeArray import ShapeArray @@ -65,6 +66,10 @@ class MultiplyObjectsJob(Job): new_location = new_location.set(z = 100 - i * 20) node.setPosition(new_location) + # Same build plate + build_plate_number = current_node.callDecoration("getBuildPlateNumber") + node.callDecoration("setBuildPlateNumber", build_plate_number) + nodes.append(node) current_progress += 1 status_message.setProgress((current_progress / total_progress) * 100) diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index 2125d731de..b0a14e41f4 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -1,14 +1,20 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator -from UM.Scene.Selection import Selection +from UM.Application import Application +from UM.Logger import Logger class BuildPlateDecorator(SceneNodeDecorator): - def __init__(self): + def __init__(self, build_plate_number = -1): super().__init__() - self._build_plate_number = -1 + self.setBuildPlateNumber(build_plate_number) def setBuildPlateNumber(self, nr): + # Make sure that groups are set correctly + # setBuildPlateForSelection in CuraActions makes sure that no single childs are set. self._build_plate_number = nr + if self._node and self._node.callDecoration("isGroup"): + for child in self._node.getChildren(): + child.callDecoration("setBuildPlateNumber", nr) def getBuildPlateNumber(self): return self._build_plate_number diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index cc27520a02..89ec2cf70d 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -35,6 +35,7 @@ Item property alias selectAll: selectAllAction; property alias deleteAll: deleteAllAction; property alias reloadAll: reloadAllAction; + property alias arrangeAllBuildPlates: arrangeAllBuildPlatesAction; property alias arrangeAll: arrangeAllAction; property alias arrangeSelection: arrangeSelectionAction; property alias resetAllTranslation: resetAllTranslationAction; @@ -300,6 +301,14 @@ Item onTriggered: CuraApplication.reloadAll(); } + Action + { + id: arrangeAllBuildPlatesAction; + text: ""; + iconName: "document-open"; + onTriggered: CuraApplication.arrangeObjectsToAllBuildPlates(); + } + Action { id: arrangeAllAction; diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index 758fa59488..4a7c84c41e 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -110,21 +110,7 @@ Rectangle { id: listview model: Cura.ObjectManager - //model: objectsListModel - - onModelChanged: - { - //currentIndex = -1; - } width: parent.width - currentIndex: -1 - onCurrentIndexChanged: - { - //base.selectedPrinter = listview.model[currentIndex]; - // Only allow connecting if the printer has responded to API query since the last refresh - //base.completeProperties = base.selectedPrinter != null && base.selectedPrinter.getProperty("incomplete") != "true"; - } - //Component.onCompleted: manager.startDiscovery() delegate: objectDelegate } } @@ -191,7 +177,7 @@ Rectangle topMargin: UM.Theme.getSize("default_margin").height; left: parent.left; leftMargin: UM.Theme.getSize("default_margin").height; - bottom: parent.bottom; + bottom: arrangeAllBuildPlatesButton.top; bottomMargin: UM.Theme.getSize("default_margin").height; } @@ -224,4 +210,46 @@ Rectangle } } + Button + { + id: arrangeAllBuildPlatesButton; + text: catalog.i18nc("@action:button","Arrange to all build plates"); + //iconSource: UM.Theme.getIcon("load") + //style: UM.Theme.styles.tool_button + height: 25 + tooltip: ''; + anchors + { + //top: buildPlateSelection.bottom; + topMargin: UM.Theme.getSize("default_margin").height; + left: parent.left; + leftMargin: UM.Theme.getSize("default_margin").height; + right: parent.right; + rightMargin: UM.Theme.getSize("default_margin").height; + bottom: arrangeBuildPlateButton.top; + bottomMargin: UM.Theme.getSize("default_margin").height; + } + action: Cura.Actions.arrangeAllBuildPlates; + } + + Button + { + id: arrangeBuildPlateButton; + text: catalog.i18nc("@action:button","Arrange current build plate"); + height: 25 + tooltip: ''; + anchors + { + topMargin: UM.Theme.getSize("default_margin").height; + left: parent.left; + leftMargin: UM.Theme.getSize("default_margin").height; + right: parent.right; + rightMargin: UM.Theme.getSize("default_margin").height; + bottom: parent.bottom; + bottomMargin: UM.Theme.getSize("default_margin").height; + } + action: Cura.Actions.arrangeAll; + } + + } diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 315b29bec0..e78cd27cee 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -376,6 +376,6 @@ "jobspecs_line": [2.0, 2.0], - "objects_menu_size": [20, 30] + "objects_menu_size": [20, 40] } }