CURA-4525 updated scene node menu and added multi buildplate arrange

This commit is contained in:
Jack Ha 2017-11-08 14:07:40 +01:00
parent 38670171f5
commit 41d5ec86a3
9 changed files with 272 additions and 44 deletions

View file

@ -30,6 +30,7 @@ class Arrange:
self._offset_x = offset_x self._offset_x = offset_x
self._offset_y = offset_y self._offset_y = offset_y
self._last_priority = 0 self._last_priority = 0
self._is_empty = True
## Helper to create an Arranger instance ## Helper to create an Arranger instance
# #
@ -38,8 +39,8 @@ class Arrange:
# \param scene_root Root for finding all scene nodes # \param scene_root Root for finding all scene nodes
# \param fixed_nodes Scene nodes to be placed # \param fixed_nodes Scene nodes to be placed
@classmethod @classmethod
def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5): def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 220, y = 220):
arranger = Arrange(220, 220, 110, 110, scale = scale) arranger = Arrange(x, y, x / 2, y / 2, scale = scale)
arranger.centerFirst() arranger.centerFirst()
if fixed_nodes is None: if fixed_nodes is None:
@ -62,7 +63,7 @@ class Arrange:
for area in disallowed_areas: for area in disallowed_areas:
points = copy.deepcopy(area._points) points = copy.deepcopy(area._points)
shape_arr = ShapeArray.fromPolygon(points, scale = scale) shape_arr = ShapeArray.fromPolygon(points, scale = scale)
arranger.place(0, 0, shape_arr) arranger.place(0, 0, shape_arr, update_empty = False)
return arranger return arranger
## Find placement for a node (using offset shape) and place it (using hull shape) ## 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 x x-coordinate
# \param y y-coordinate # \param y y-coordinate
# \param shape_arr ShapeArray object # \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) x = int(self._scale * x)
y = int(self._scale * y) y = int(self._scale * y)
offset_x = x + self._offset_x + shape_arr.offset_x 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) 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] 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 # we use a slice of shape because it can be out of bounds
occupied_slice[numpy.where(shape_arr.arr[ new_occupied = numpy.where(shape_arr.arr[
min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 1 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. # 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 = self._priority[min_y:max_y, min_x:max_x]
prio_slice[numpy.where(shape_arr.arr[ 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 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

View file

@ -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()

View file

@ -134,25 +134,16 @@ class CuraActions(QObject):
Logger.log("d", "Setting build plate number... %d" % build_plate_nr) Logger.log("d", "Setting build plate number... %d" % build_plate_nr)
operation = GroupedOperation() operation = GroupedOperation()
root = Application.getInstance().getController().getScene().getRoot()
nodes_to_change = [] nodes_to_change = []
for node in Selection.getAllSelectedObjects(): for node in Selection.getAllSelectedObjects():
# Do not change any nodes that already have the right extruder set. parent_node = node # Find the parent node to change instead
if node.callDecoration("getBuildPlateNumber") == build_plate_nr: while parent_node.getParent() != root:
continue parent_node = parent_node.getParent()
# If the node is a group, apply the active extruder to all children of the group. for single_node in BreadthFirstIterator(parent_node):
if node.callDecoration("isGroup"): nodes_to_change.append(single_node)
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)
if not nodes_to_change: if not nodes_to_change:
Logger.log("d", "Nothing to change.") Logger.log("d", "Nothing to change.")

View file

@ -39,8 +39,11 @@ from cura.ConvexHullDecorator import ConvexHullDecorator
from cura.SetParentOperation import SetParentOperation from cura.SetParentOperation import SetParentOperation
from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.SliceableObjectDecorator import SliceableObjectDecorator
from cura.BlockSlicingDecorator import BlockSlicingDecorator from cura.BlockSlicingDecorator import BlockSlicingDecorator
# research
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.ArrangeObjectsJob import ArrangeObjectsJob from cura.ArrangeObjectsJob import ArrangeObjectsJob
from cura.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
from cura.MultiplyObjectsJob import MultiplyObjectsJob from cura.MultiplyObjectsJob import MultiplyObjectsJob
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType 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.UserProfilesModel import UserProfilesModel
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
# research
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from . import PlatformPhysics from . import PlatformPhysics
from . import BuildVolume from . import BuildVolume
@ -1105,7 +1106,7 @@ class CuraApplication(QtApplication):
## Arrange all objects. ## Arrange all objects.
@pyqtSlot() @pyqtSlot()
def arrangeAll(self): def arrangeObjectsToAllBuildPlates(self):
nodes = [] nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if type(node) is not SceneNode: if type(node) is not SceneNode:
@ -1119,6 +1120,26 @@ class CuraApplication(QtApplication):
# Skip nodes that are too big # Skip nodes that are too big
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
nodes.append(node) 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 = []) self.arrange(nodes, fixed_nodes = [])
## Arrange Selection ## Arrange Selection
@ -1250,6 +1271,7 @@ class CuraApplication(QtApplication):
group_decorator = GroupDecorator() group_decorator = GroupDecorator()
group_node.addDecorator(group_decorator) group_node.addDecorator(group_decorator)
group_node.addDecorator(ConvexHullDecorator()) group_node.addDecorator(ConvexHullDecorator())
group_node.addDecorator(BuildPlateDecorator(self.activeBuildPlate))
group_node.setParent(self.getController().getScene().getRoot()) group_node.setParent(self.getController().getScene().getRoot())
group_node.setSelectable(True) group_node.setSelectable(True)
center = Selection.getSelectionCenter() center = Selection.getSelectionCenter()

View file

@ -13,6 +13,7 @@ from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
from cura.ZOffsetDecorator import ZOffsetDecorator from cura.ZOffsetDecorator import ZOffsetDecorator
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Arrange import Arrange from cura.Arrange import Arrange
from cura.ShapeArray import ShapeArray from cura.ShapeArray import ShapeArray
@ -65,6 +66,10 @@ class MultiplyObjectsJob(Job):
new_location = new_location.set(z = 100 - i * 20) new_location = new_location.set(z = 100 - i * 20)
node.setPosition(new_location) node.setPosition(new_location)
# Same build plate
build_plate_number = current_node.callDecoration("getBuildPlateNumber")
node.callDecoration("setBuildPlateNumber", build_plate_number)
nodes.append(node) nodes.append(node)
current_progress += 1 current_progress += 1
status_message.setProgress((current_progress / total_progress) * 100) status_message.setProgress((current_progress / total_progress) * 100)

View file

@ -1,14 +1,20 @@
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator 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): class BuildPlateDecorator(SceneNodeDecorator):
def __init__(self): def __init__(self, build_plate_number = -1):
super().__init__() super().__init__()
self._build_plate_number = -1 self.setBuildPlateNumber(build_plate_number)
def setBuildPlateNumber(self, nr): 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 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): def getBuildPlateNumber(self):
return self._build_plate_number return self._build_plate_number

View file

@ -35,6 +35,7 @@ Item
property alias selectAll: selectAllAction; property alias selectAll: selectAllAction;
property alias deleteAll: deleteAllAction; property alias deleteAll: deleteAllAction;
property alias reloadAll: reloadAllAction; property alias reloadAll: reloadAllAction;
property alias arrangeAllBuildPlates: arrangeAllBuildPlatesAction;
property alias arrangeAll: arrangeAllAction; property alias arrangeAll: arrangeAllAction;
property alias arrangeSelection: arrangeSelectionAction; property alias arrangeSelection: arrangeSelectionAction;
property alias resetAllTranslation: resetAllTranslationAction; property alias resetAllTranslation: resetAllTranslationAction;
@ -300,6 +301,14 @@ Item
onTriggered: CuraApplication.reloadAll(); onTriggered: CuraApplication.reloadAll();
} }
Action
{
id: arrangeAllBuildPlatesAction;
text: "";
iconName: "document-open";
onTriggered: CuraApplication.arrangeObjectsToAllBuildPlates();
}
Action Action
{ {
id: arrangeAllAction; id: arrangeAllAction;

View file

@ -110,21 +110,7 @@ Rectangle
{ {
id: listview id: listview
model: Cura.ObjectManager model: Cura.ObjectManager
//model: objectsListModel
onModelChanged:
{
//currentIndex = -1;
}
width: parent.width 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 delegate: objectDelegate
} }
} }
@ -191,7 +177,7 @@ Rectangle
topMargin: UM.Theme.getSize("default_margin").height; topMargin: UM.Theme.getSize("default_margin").height;
left: parent.left; left: parent.left;
leftMargin: UM.Theme.getSize("default_margin").height; leftMargin: UM.Theme.getSize("default_margin").height;
bottom: parent.bottom; bottom: arrangeAllBuildPlatesButton.top;
bottomMargin: UM.Theme.getSize("default_margin").height; 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;
}
} }

View file

@ -376,6 +376,6 @@
"jobspecs_line": [2.0, 2.0], "jobspecs_line": [2.0, 2.0],
"objects_menu_size": [20, 30] "objects_menu_size": [20, 40]
} }
} }