From 5b368fbfd5e8c588b6679c62fb81f20a47052489 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 6 Nov 2017 09:25:42 +0100 Subject: [PATCH 01/42] CURA-4104 added first sucky build plate selection options --- cura/BuildPlateModel.py | 2 + cura/CuraActions.py | 43 +++++++++++++++++++ cura/CuraApplication.py | 36 +++++++++++++++- .../SetBuildPlateNumberOperation.py | 27 ++++++++++++ cura/Scene/BuildPlateDecorator.py | 34 +++++++++++++++ resources/qml/Menus/ContextMenu.qml | 20 +++++++++ resources/qml/Menus/ViewMenu.qml | 19 ++++++++ 7 files changed, 179 insertions(+), 2 deletions(-) create mode 100644 cura/BuildPlateModel.py create mode 100644 cura/Operations/SetBuildPlateNumberOperation.py create mode 100644 cura/Scene/BuildPlateDecorator.py diff --git a/cura/BuildPlateModel.py b/cura/BuildPlateModel.py new file mode 100644 index 0000000000..139597f9cb --- /dev/null +++ b/cura/BuildPlateModel.py @@ -0,0 +1,2 @@ + + diff --git a/cura/CuraActions.py b/cura/CuraActions.py index b51728f028..663da3ec09 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -19,6 +19,11 @@ from cura.MultiplyObjectsJob import MultiplyObjectsJob from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation from cura.Settings.ExtruderManager import ExtruderManager +from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation + +from UM.Logger import Logger + + class CuraActions(QObject): def __init__(self, parent = None): super().__init__(parent) @@ -124,5 +129,43 @@ class CuraActions(QObject): operation.addOperation(SetObjectExtruderOperation(node, extruder_id)) operation.push() + @pyqtSlot(int) + def setBuildPlateForSelection(self, build_plate_nr: int) -> None: + Logger.log("d", "Setting build plate number... %d" % build_plate_nr) + operation = GroupedOperation() + + 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 + + # 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) + + if not nodes_to_change: + Logger.log("d", "Nothing to change.") + # If there are no changes to make, we still need to reset the selected extruders. + # This is a workaround for checked menu items being deselected while still being + # selected. + #ExtruderManager.getInstance().resetSelectedObjectExtruders() + return + + Logger.log("d", "Yes: %s", nodes_to_change) + for node in nodes_to_change: + operation.addOperation(SetBuildPlateNumberOperation(node, build_plate_nr)) + operation.push() + def _openUrl(self, url): QDesktopServices.openUrl(url) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index b09371ae0d..ff8dfd021a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1,4 +1,5 @@ # Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtNetwork import QLocalServer @@ -53,6 +54,9 @@ 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 from . import CameraAnimation @@ -376,6 +380,10 @@ class CuraApplication(QtApplication): self._plugin_registry.addSupportedPluginExtension("curaplugin", "Cura Plugin") + # research + self._num_build_plates = 1 # default + self._active_build_plate = 1 + def _onEngineCreated(self): self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) @@ -855,6 +863,8 @@ class CuraApplication(QtApplication): activityChanged = pyqtSignal() sceneBoundingBoxChanged = pyqtSignal() preferredOutputMimetypeChanged = pyqtSignal() + numBuildPlatesChanged = pyqtSignal() + activeBuildPlateChanged = pyqtSignal() @pyqtProperty(bool, notify = activityChanged) def platformActivity(self): @@ -1025,7 +1035,7 @@ class CuraApplication(QtApplication): op.push() Selection.clear() - ## Reset all translation on nodes with mesh data. + ## Reset all translation on nodes with mesh data. @pyqtSlot() def resetAllTranslation(self): Logger.log("i", "Resetting all scene translations") @@ -1150,7 +1160,7 @@ class CuraApplication(QtApplication): job.start() else: Logger.log("w", "Unable to reload data because we don't have a filename.") - + ## Get logging data of the backend engine # \returns \type{string} Logging data @pyqtSlot(result = str) @@ -1373,6 +1383,7 @@ class CuraApplication(QtApplication): for node in nodes: node.setSelectable(True) node.setName(os.path.basename(filename)) + node.addDecorator(BuildPlateDecorator()) extension = os.path.splitext(filename)[1] if extension.lower() in self._non_sliceable_extensions: @@ -1445,3 +1456,24 @@ class CuraApplication(QtApplication): node = node.getParent() Selection.add(node) + + #### research - hacky place for these kind of thing + @pyqtSlot(int) + def setActiveBuildPlate(self, nr): + Logger.log("d", "Select build plate: %s" % nr) + self._active_build_plate = nr + self.activeBuildPlateChanged.emit() + + @pyqtSlot() + def newBuildPlate(self): + Logger.log("d", "New build plate") + self._num_build_plates += 1 + self.numBuildPlatesChanged.emit() + + @pyqtProperty(int, notify = numBuildPlatesChanged) + def numBuildPlates(self): + return self._num_build_plates + + @pyqtProperty(int, notify = activeBuildPlateChanged) + def activeBuildPlate(self): + return self._active_build_plate diff --git a/cura/Operations/SetBuildPlateNumberOperation.py b/cura/Operations/SetBuildPlateNumberOperation.py new file mode 100644 index 0000000000..bbef4caf84 --- /dev/null +++ b/cura/Operations/SetBuildPlateNumberOperation.py @@ -0,0 +1,27 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from UM.Scene.SceneNode import SceneNode +from UM.Operations.Operation import Operation + +from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator + +## Simple operation to set the extruder a certain object should be printed with. +class SetBuildPlateNumberOperation(Operation): + def __init__(self, node: SceneNode, build_plate_nr: int) -> None: + self._node = node + self._build_plate_nr = build_plate_nr + self._previous_build_plate_nr = None + self._decorator_added = False + + def undo(self): + if self._previous_build_plate_nr: + self._node.callDecoration("setBuildPlateNumber", self._previous_build_plate_nr) + + def redo(self): + stack = self._node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway. + if not stack: + self._node.addDecorator(SettingOverrideDecorator()) + + self._previous_build_plate_nr = self._node.callDecoration("getBuildPlateNumber") + self._node.callDecoration("setBuildPlateNumber", self._build_plate_nr) diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py new file mode 100644 index 0000000000..8d91f9e90a --- /dev/null +++ b/cura/Scene/BuildPlateDecorator.py @@ -0,0 +1,34 @@ +from UM.Scene.SceneNodeDecorator import SceneNodeDecorator +from UM.Scene.Selection import Selection + + +class BuildPlateDecorator(SceneNodeDecorator): + def __init__(self): + super().__init__() + self._build_plate_number = -1 + + def setBuildPlateNumber(self, nr): + self._build_plate_number = nr + # self.getNode().childrenChanged.connect(self._onChildrenChanged) + + def getBuildPlateNumber(self): + return self._build_plate_number + + # def setNode(self, node): + # super().setNode(node) + # self.getNode().childrenChanged.connect(self._onChildrenChanged) + + # def _onChildrenChanged(self, node): + # if not self.getNode().hasChildren(): + # # A group that no longer has children may remove itself from the scene + # self._old_parent = self.getNode().getParent() + # self.getNode().setParent(None) + # Selection.remove(self.getNode()) + # else: + # # A group that has removed itself from the scene because it had no children may add itself back to the scene when a child is added to it + # if not self.getNode().getParent() and self._old_parent: + # self.getNode().setParent(self._old_parent) + # self._old_parent = None + + def __deepcopy__(self, memo): + return BuildPlateDecorator() diff --git a/resources/qml/Menus/ContextMenu.qml b/resources/qml/Menus/ContextMenu.qml index 39d497722f..175410773e 100644 --- a/resources/qml/Menus/ContextMenu.qml +++ b/resources/qml/Menus/ContextMenu.qml @@ -39,6 +39,26 @@ Menu onObjectRemoved: base.removeItem(object) } + MenuSeparator {} + MenuItem { + text: "build plate 0"; + onTriggered: CuraActions.setBuildPlateForSelection(0); + checkable: true + checked: false + } + MenuItem { + text: "build plate 1"; + onTriggered: CuraActions.setBuildPlateForSelection(1); + checkable: true + checked: false + } + MenuItem { + text: "build plate 2"; + onTriggered: CuraActions.setBuildPlateForSelection(2); + checkable: true + checked: false + } + // Global actions MenuSeparator {} MenuItem { action: Cura.Actions.selectAll; } diff --git a/resources/qml/Menus/ViewMenu.qml b/resources/qml/Menus/ViewMenu.qml index bb5999edb9..3c5485da32 100644 --- a/resources/qml/Menus/ViewMenu.qml +++ b/resources/qml/Menus/ViewMenu.qml @@ -28,6 +28,25 @@ Menu } ExclusiveGroup { id: group; } + MenuSeparator {} + MenuItem { + text: "build plate 0"; + onTriggered: CuraApplication.setActiveBuildPlate(0); + } + MenuItem { + text: "build plate 1"; + onTriggered: CuraApplication.setActiveBuildPlate(1); + } + MenuItem { + text: "build plate 2"; + onTriggered: CuraApplication.setActiveBuildPlate(2); + } + ExclusiveGroup { id: buildPlateGroup; } + + MenuItem { + text: "New build plate"; + onTriggered: CuraApplication.newBuildPlate(); + } MenuSeparator {} MenuItem { action: Cura.Actions.homeCamera; } } From 5050124699fa50501916fe7319417e480efd3a94 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 6 Nov 2017 14:02:22 +0100 Subject: [PATCH 02/42] CURA-4525 wip objects menu --- cura/CuraApplication.py | 4 + plugins/CuraEngineBackend/StartSliceJob.py | 10 ++- plugins/SolidView/SolidView.py | 3 +- resources/qml/Cura.qml | 41 +++++++++- resources/qml/ObjectsList.qml | 88 ++++++++++++++++++++++ resources/themes/cura-light/theme.json | 4 +- 6 files changed, 144 insertions(+), 6 deletions(-) create mode 100644 resources/qml/ObjectsList.qml diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index ff8dfd021a..5111b9b52a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -292,6 +292,8 @@ class CuraApplication(QtApplication): preferences.addPreference("metadata/setting_version", 0) preferences.setValue("metadata/setting_version", self.SettingVersion) #Don't make it equal to the default so that the setting version always gets written to the file. + preferences.addPreference("view/build_plate_number", 0) + preferences.addPreference("cura/active_mode", "simple") preferences.addPreference("cura/categories_expanded", "") @@ -1462,6 +1464,8 @@ class CuraApplication(QtApplication): def setActiveBuildPlate(self, nr): Logger.log("d", "Select build plate: %s" % nr) self._active_build_plate = nr + Preferences.setValue("view/build_plate_number", self._active_build_plate) + self.activeBuildPlateChanged.emit() @pyqtSlot() diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index a53daa4e63..607914f5c5 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -140,9 +140,13 @@ class StartSliceJob(Job): temp_list = [] for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None: - if not getattr(node, "_outside_buildarea", False)\ - or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)): - temp_list.append(node) + + # temp hack to filter on build plate 0 + if (node.callDecoration("getBuildPlateNumber") == 0): + + if not getattr(node, "_outside_buildarea", False)\ + or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)): + temp_list.append(node) Job.yieldThread() if temp_list: diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index 8f0c9a4dc1..625223a097 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -71,10 +71,11 @@ class SolidView(View): else: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) + activeBuildPlateNumber = Preferences.getInstance().getValue("view/build_plate_number") or 0 for node in DepthFirstIterator(scene.getRoot()): if not node.render(renderer): - if node.getMeshData() and node.isVisible(): + if node.getMeshData() and node.isVisible() and (node.callDecoration("getBuildPlateNumber") == activeBuildPlateNumber): uniforms = {} shade_factor = 1.0 diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 2fd19a8a03..03d0ce9ecd 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -340,6 +340,22 @@ UM.MainWindow action: Cura.Actions.open; } + Button + { + id: objectsButton; + text: catalog.i18nc("@action:button","Objects"); + iconSource: UM.Theme.getIcon("load") + style: UM.Theme.styles.tool_button + tooltip: ''; + anchors + { + top: openFileButton.bottom; + topMargin: UM.Theme.getSize("default_margin").height; + left: parent.left; + } + action: triggerObjectsList; + } + Toolbar { id: toolbar; @@ -348,7 +364,7 @@ UM.MainWindow property int mouseY: base.mouseY anchors { - top: openFileButton.bottom; + top: objectsButton.bottom; topMargin: UM.Theme.getSize("window_margin").height; left: parent.left; } @@ -380,6 +396,29 @@ UM.MainWindow monitoringPrint: base.showPrintMonitor } + Action + { + id: triggerObjectsList; + text: catalog.i18nc("@action:inmenu menubar:file","&Open File(s)..."); + iconName: "document-open"; + shortcut: StandardKey.Open; + onTriggered: objectsList.visible = !objectsList.visible; + } + + ObjectsList + { + id: objectsList; + visible: false; + anchors + { + top: objectsButton.top; + left: objectsButton.right; + leftMargin: UM.Theme.getSize("default_margin").width; + rightMargin: UM.Theme.getSize("default_margin").width; + } + + } + Rectangle { id: viewportOverlay diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml new file mode 100644 index 0000000000..105bdb957b --- /dev/null +++ b/resources/qml/ObjectsList.qml @@ -0,0 +1,88 @@ +// Copyright (c) 2017 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Controls.Styles 1.1 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.1 + +import UM 1.3 as UM +import Cura 1.0 as Cura + +import "Menus" + +Rectangle +{ + id: base; + + color: UM.Theme.getColor("tool_panel_background") + + width: UM.Theme.getSize("objects_menu_size").width + height: UM.Theme.getSize("objects_menu_size").height + + Button + { + id: openFileButton; + text: catalog.i18nc("@action:button","Open File"); + iconSource: UM.Theme.getIcon("load") + style: UM.Theme.styles.tool_button + tooltip: ''; + anchors + { + top: parent.top; + topMargin: UM.Theme.getSize("default_margin").height; + left: parent.left; + leftMargin: UM.Theme.getSize("default_margin").height; + } + action: Cura.Actions.open; + } + + ListModel + { + id: objectsListModel; + + ListElement { + name: "Apple" + cost: 2.45 + } + ListElement { + name: "Orange" + cost: 3.25 + } + ListElement { + name: "Banana" + cost: 1.95 + } + } + + Component { + id: objectDelegate + Rectangle { + height: 30 + + Text { + text: name + color: red + } + //Text { text: '$' + cost } + } + } + + ListView + { + model: objectsListModel; + anchors + { + top: openFileButton.bottom; + topMargin: UM.Theme.getSize("default_margin").height; + left: parent.left; + leftMargin: UM.Theme.getSize("default_margin").height; + } + width: parent.width - 2 * UM.Theme.getSize("default_margin").height + height: 100 + + delegate: objectDelegate + } + +} diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index b41ea96846..315b29bec0 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -374,6 +374,8 @@ "infill_button_margin": [0.5, 0.5], - "jobspecs_line": [2.0, 2.0] + "jobspecs_line": [2.0, 2.0], + + "objects_menu_size": [20, 30] } } From 38670171f5063a0aa525117955da39135bca2a20 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 7 Nov 2017 14:52:22 +0100 Subject: [PATCH 03/42] CURA-4525 party working objects list and build plates --- cura/CuraActions.py | 5 - cura/CuraApplication.py | 17 ++- cura/ObjectManager.py | 68 ++++++++++ cura/Scene/BuildPlateDecorator.py | 17 --- plugins/SolidView/SolidView.py | 2 +- resources/qml/ObjectsList.qml | 201 +++++++++++++++++++++++++----- 6 files changed, 252 insertions(+), 58 deletions(-) create mode 100644 cura/ObjectManager.py diff --git a/cura/CuraActions.py b/cura/CuraActions.py index 663da3ec09..c313488cac 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -156,13 +156,8 @@ class CuraActions(QObject): if not nodes_to_change: Logger.log("d", "Nothing to change.") - # If there are no changes to make, we still need to reset the selected extruders. - # This is a workaround for checked menu items being deselected while still being - # selected. - #ExtruderManager.getInstance().resetSelectedObjectExtruders() return - Logger.log("d", "Yes: %s", nodes_to_change) for node in nodes_to_change: operation.addOperation(SetBuildPlateNumberOperation(node, build_plate_nr)) operation.push() diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 5111b9b52a..7a6994e83e 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -79,6 +79,8 @@ from cura.Settings.ContainerManager import ContainerManager from cura.Settings.GlobalStack import GlobalStack from cura.Settings.ExtruderStack import ExtruderStack +from cura.ObjectManager import ObjectManager + from PyQt5.QtCore import QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS from UM.FlameProfiler import pyqtSlot from PyQt5.QtGui import QColor, QIcon @@ -205,6 +207,7 @@ class CuraApplication(QtApplication): self._machine_action_manager = MachineActionManager.MachineActionManager() self._machine_manager = None # This is initialized on demand. self._material_manager = None + self._object_manager = None self._setting_inheritance_manager = None self._simple_mode_settings_manager = None @@ -292,8 +295,6 @@ class CuraApplication(QtApplication): preferences.addPreference("metadata/setting_version", 0) preferences.setValue("metadata/setting_version", self.SettingVersion) #Don't make it equal to the default so that the setting version always gets written to the file. - preferences.addPreference("view/build_plate_number", 0) - preferences.addPreference("cura/active_mode", "simple") preferences.addPreference("cura/categories_expanded", "") @@ -384,7 +385,7 @@ class CuraApplication(QtApplication): # research self._num_build_plates = 1 # default - self._active_build_plate = 1 + self._active_build_plate = 0 def _onEngineCreated(self): self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) @@ -717,6 +718,9 @@ class CuraApplication(QtApplication): qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager) + Logger.log("d", " #### going to register object manager") + qmlRegisterSingletonType(ObjectManager, "Cura", 1, 2, "ObjectManager", self.getObjectManager) + qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager) self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml")) self._qml_import_paths.append(Resources.getPath(self.ResourceTypes.QmlFiles)) @@ -744,6 +748,11 @@ class CuraApplication(QtApplication): self._material_manager = MaterialManager.createMaterialManager() return self._material_manager + def getObjectManager(self, *args): + if self._object_manager is None: + self._object_manager = ObjectManager.createObjectManager() + return self._object_manager + def getSettingInheritanceManager(self, *args): if self._setting_inheritance_manager is None: self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager() @@ -799,6 +808,7 @@ class CuraApplication(QtApplication): qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel") qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator") qmlRegisterType(UserChangesModel, "Cura", 1, 1, "UserChangesModel") + # qmlRegisterSingletonType(ObjectManager, "Cura", 1, 0, "ObjectManager", ObjectManager.createObjectManager) qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager) @@ -1464,7 +1474,6 @@ class CuraApplication(QtApplication): def setActiveBuildPlate(self, nr): Logger.log("d", "Select build plate: %s" % nr) self._active_build_plate = nr - Preferences.setValue("view/build_plate_number", self._active_build_plate) self.activeBuildPlateChanged.emit() diff --git a/cura/ObjectManager.py b/cura/ObjectManager.py new file mode 100644 index 0000000000..fc6d343252 --- /dev/null +++ b/cura/ObjectManager.py @@ -0,0 +1,68 @@ +from UM.Logger import Logger +from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot +from UM.Application import Application +from UM.Qt.ListModel import ListModel +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +from UM.Scene.SceneNode import SceneNode +from UM.Scene.Selection import Selection +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QApplication + + +class ObjectManager(ListModel): + def __init__(self): + super().__init__() + self._last_selected_index = 0 + Application.getInstance().getController().getScene().sceneChanged.connect(self._update) + + def _update(self, *args): + nodes = [] + for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): + if type(node) is not SceneNode or (not node.getMeshData() and not node.callDecoration("getLayerData")): + continue + nodes.append({ + "name": node.getName(), + "isSelected": Selection.isSelected(node), + "buildPlateNumber": node.callDecoration("getBuildPlateNumber"), + "node": node + }) + nodes = sorted(nodes, key=lambda n: n["name"]) + self.setItems(nodes) + + self.itemsChanged.emit() + + ## Either select or deselect an item + @pyqtSlot(int) + def changeSelection(self, index): + modifiers = QApplication.keyboardModifiers() + ctrl_is_active = modifiers & Qt.ControlModifier + shift_is_active = modifiers & Qt.ShiftModifier + + if ctrl_is_active: + item = self.getItem(index) + node = item["node"] + if Selection.isSelected(node): + Selection.remove(node) + else: + Selection.add(node) + elif shift_is_active: + polarity = 1 if index + 1 > self._last_selected_index else -1 + for i in range(self._last_selected_index, index + polarity, polarity): + item = self.getItem(i) + node = item["node"] + Selection.add(node) + else: + # Single select + item = self.getItem(index) + node = item["node"] + Selection.clear() + Selection.add(node) + build_plate_number = node.callDecoration("getBuildPlateNumber") + if build_plate_number is not None and build_plate_number != -1: + Application.getInstance().setActiveBuildPlate(build_plate_number) + + self._last_selected_index = index + + @staticmethod + def createObjectManager(): + return ObjectManager() diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index 8d91f9e90a..2125d731de 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -9,26 +9,9 @@ class BuildPlateDecorator(SceneNodeDecorator): def setBuildPlateNumber(self, nr): self._build_plate_number = nr - # self.getNode().childrenChanged.connect(self._onChildrenChanged) def getBuildPlateNumber(self): return self._build_plate_number - # def setNode(self, node): - # super().setNode(node) - # self.getNode().childrenChanged.connect(self._onChildrenChanged) - - # def _onChildrenChanged(self, node): - # if not self.getNode().hasChildren(): - # # A group that no longer has children may remove itself from the scene - # self._old_parent = self.getNode().getParent() - # self.getNode().setParent(None) - # Selection.remove(self.getNode()) - # else: - # # A group that has removed itself from the scene because it had no children may add itself back to the scene when a child is added to it - # if not self.getNode().getParent() and self._old_parent: - # self.getNode().setParent(self._old_parent) - # self._old_parent = None - def __deepcopy__(self, memo): return BuildPlateDecorator() diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index 625223a097..d37fbb7c9d 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -71,7 +71,7 @@ class SolidView(View): else: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) - activeBuildPlateNumber = Preferences.getInstance().getValue("view/build_plate_number") or 0 + activeBuildPlateNumber = Application.getInstance().activeBuildPlate for node in DepthFirstIterator(scene.getRoot()): if not node.render(renderer): diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index 105bdb957b..758fa59488 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -8,7 +8,7 @@ import QtQuick.Layouts 1.1 import QtQuick.Dialogs 1.1 import UM 1.3 as UM -import Cura 1.0 as Cura +import Cura 1.2 as Cura import "Menus" @@ -21,6 +21,8 @@ Rectangle width: UM.Theme.getSize("objects_menu_size").width height: UM.Theme.getSize("objects_menu_size").height + SystemPalette { id: palette } + Button { id: openFileButton; @@ -38,51 +40,188 @@ Rectangle action: Cura.Actions.open; } - ListModel - { - id: objectsListModel; - - ListElement { - name: "Apple" - cost: 2.45 - } - ListElement { - name: "Orange" - cost: 3.25 - } - ListElement { - name: "Banana" - cost: 1.95 - } - } - Component { id: objectDelegate - Rectangle { - height: 30 + Rectangle + { + height: childrenRect.height + color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlight : index % 2 ? palette.base : palette.alternateBase + width: parent.width + Label + { + id: nodeNameLabel + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + //anchors.right: parent.right + width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30 + text: Cura.ObjectManager.getItem(index).name; + color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : palette.text + elide: Text.ElideRight + } - Text { - text: name - color: red + Label + { + id: buildPlateNumberLabel + width: 20 + anchors.left: nodeNameLabel.right + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: parent.right + text: Cura.ObjectManager.getItem(index).buildPlateNumber; + color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : palette.text + elide: Text.ElideRight + } + + MouseArea + { + anchors.fill: parent; + onClicked: + { + Cura.ObjectManager.changeSelection(index); + } + } } - //Text { text: '$' + cost } - } } - ListView + // list all the scene nodes + ScrollView { - model: objectsListModel; + id: objectsList + frameVisible: true + width: parent.width - 2 * UM.Theme.getSize("default_margin").height + anchors { top: openFileButton.bottom; topMargin: UM.Theme.getSize("default_margin").height; left: parent.left; leftMargin: UM.Theme.getSize("default_margin").height; + bottom: buildPlateSelection.top; + bottomMargin: UM.Theme.getSize("default_margin").height; } - width: parent.width - 2 * UM.Theme.getSize("default_margin").height - height: 100 - delegate: objectDelegate + Rectangle + { + parent: viewport + anchors.fill: parent + color: palette.light + } + + ListView + { + 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 + } + } + + ListModel + { + id: buildPlatesModel + + ListElement + { + name: "build plate 0" + buildPlateNumber: 0 + } + ListElement + { + name: "build plate 1" + buildPlateNumber: 1 + } + ListElement + { + name: "build plate 2" + buildPlateNumber: 2 + } + } + + Component { + id: buildPlateDelegate + Rectangle + { + height: childrenRect.height + color: CuraApplication.activeBuildPlate == buildPlateNumber ? palette.highlight : index % 2 ? palette.base : palette.alternateBase + width: parent.width + Label + { + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: parent.right + text: name //Cura.ObjectManager.getItem(index).name; + color: CuraApplication.activeBuildPlate == buildPlateNumber ? palette.highlightedText : palette.text + elide: Text.ElideRight + } + + MouseArea + { + anchors.fill: parent; + onClicked: + { + CuraApplication.setActiveBuildPlate(buildPlateNumber); + } + } + } + } + + ScrollView + { + id: buildPlateSelection + frameVisible: true + height: 100 + width: parent.width - 2 * UM.Theme.getSize("default_margin").height + + anchors + { + // top: objectsList.bottom; + topMargin: UM.Theme.getSize("default_margin").height; + left: parent.left; + leftMargin: UM.Theme.getSize("default_margin").height; + bottom: parent.bottom; + bottomMargin: UM.Theme.getSize("default_margin").height; + } + + Rectangle + { + parent: viewport + anchors.fill: parent + color: palette.light + } + + ListView + { + id: buildPlateListView + model: buildPlatesModel + + 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: buildPlateDelegate + } } } From 41d5ec86a305d4fc0d5f85e159631d4936032b93 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 8 Nov 2017 14:07:40 +0100 Subject: [PATCH 04/42] CURA-4525 updated scene node menu and added multi buildplate arrange --- cura/Arrange.py | 20 ++- cura/ArrangeObjectsAllBuildPlatesJob.py | 159 ++++++++++++++++++++++++ cura/CuraActions.py | 23 ++-- cura/CuraApplication.py | 28 ++++- cura/MultiplyObjectsJob.py | 5 + cura/Scene/BuildPlateDecorator.py | 12 +- resources/qml/Actions.qml | 9 ++ resources/qml/ObjectsList.qml | 58 ++++++--- resources/themes/cura-light/theme.json | 2 +- 9 files changed, 272 insertions(+), 44 deletions(-) create mode 100644 cura/ArrangeObjectsAllBuildPlatesJob.py 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] } } From e21acd1a07da8bd40031c0039dc5abc831f276ec Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 9 Nov 2017 17:03:20 +0100 Subject: [PATCH 05/42] CURA-4525 first multi slice + multi layer data, added filter on build plate, added option arrange on load, visuals like convex hull are now correct --- cura/Arrange.py | 2 +- cura/ArrangeObjectsAllBuildPlatesJob.py | 1 - cura/ConvexHullNode.py | 3 +- cura/CuraApplication.py | 105 +++--- cura/ObjectManager.py | 29 +- cura/Scene/BuildPlateDecorator.py | 7 + cura/Scene/CuraSceneNode.py | 40 +++ plugins/3MFReader/ThreeMFReader.py | 3 +- .../CuraEngineBackend/CuraEngineBackend.py | 140 +++++--- .../ProcessSlicedLayersJob.py | 27 +- plugins/CuraEngineBackend/StartSliceJob.py | 19 +- plugins/ImageReader/ImageReader.py | 4 +- plugins/LayerView/LayerPass.py | 3 +- plugins/SolidView/SolidView.py | 2 +- plugins/X3DReader/X3DReader.py | 303 +++++++++--------- resources/qml/Cura.qml | 1 + resources/qml/ObjectsList.qml | 23 +- resources/qml/Preferences/GeneralPage.qml | 16 +- 18 files changed, 468 insertions(+), 260 deletions(-) create mode 100644 cura/Scene/CuraSceneNode.py diff --git a/cura/Arrange.py b/cura/Arrange.py index 305729d763..2f77ec9a7f 100755 --- a/cura/Arrange.py +++ b/cura/Arrange.py @@ -40,7 +40,7 @@ class Arrange: # \param fixed_nodes Scene nodes to be placed @classmethod 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 = Arrange(x, y, x // 2, y // 2, scale = scale) arranger.centerFirst() if fixed_nodes is None: diff --git a/cura/ArrangeObjectsAllBuildPlatesJob.py b/cura/ArrangeObjectsAllBuildPlatesJob.py index eacd18d5ad..7991ac39f0 100644 --- a/cura/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/ArrangeObjectsAllBuildPlatesJob.py @@ -112,7 +112,6 @@ class ArrangeObjectsAllBuildPlatesJob(Job): # 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() diff --git a/cura/ConvexHullNode.py b/cura/ConvexHullNode.py index cc4720c197..c6ff80670d 100644 --- a/cura/ConvexHullNode.py +++ b/cura/ConvexHullNode.py @@ -6,7 +6,6 @@ from UM.Scene.SceneNode import SceneNode from UM.Resources import Resources from UM.Math.Color import Color from UM.Mesh.MeshBuilder import MeshBuilder # To create a mesh to display the convex hull with. - from UM.View.GL.OpenGL import OpenGL @@ -65,7 +64,7 @@ class ConvexHullNode(SceneNode): ConvexHullNode.shader.setUniformValue("u_diffuseColor", self._color) ConvexHullNode.shader.setUniformValue("u_opacity", 0.6) - if self.getParent(): + if self.getParent() and self.getParent().callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate: if self.getMeshData(): renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8) if self._convex_hull_head_mesh: diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 39e4b38824..c1c894c735 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -33,6 +33,7 @@ from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.SetTransformOperation import SetTransformOperation + from cura.Arrange import Arrange from cura.ShapeArray import ShapeArray from cura.ConvexHullDecorator import ConvexHullDecorator @@ -41,6 +42,7 @@ from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.BlockSlicingDecorator import BlockSlicingDecorator # research from cura.Scene.BuildPlateDecorator import BuildPlateDecorator +from cura.Scene.CuraSceneNode import CuraSceneNode from cura.ArrangeObjectsJob import ArrangeObjectsJob from cura.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob @@ -307,11 +309,13 @@ class CuraApplication(QtApplication): preferences.addPreference("cura/asked_dialog_on_project_save", False) preferences.addPreference("cura/choice_on_profile_override", "always_ask") preferences.addPreference("cura/choice_on_open_project", "always_ask") + preferences.addPreference("cura/arrange_objects_on_load", True) preferences.addPreference("cura/currency", "€") preferences.addPreference("cura/material_settings", "{}") preferences.addPreference("view/invert_zoom", False) + preferences.addPreference("view/filter_current_build_plate", False) self._need_to_show_user_agreement = not Preferences.getInstance().getValue("general/accepted_user_agreement") @@ -896,7 +900,7 @@ class CuraApplication(QtApplication): scene_bounding_box = None is_block_slicing_node = False for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode or (not node.getMeshData() and not node.callDecoration("getLayerData")): + if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")): continue if node.callDecoration("isBlockSlicing"): is_block_slicing_node = True @@ -1013,7 +1017,7 @@ class CuraApplication(QtApplication): Selection.clear() for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: + if not issubclass(type(node), SceneNode): continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. @@ -1021,6 +1025,9 @@ class CuraApplication(QtApplication): 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 not node.callDecoration("isSliceable"): + continue # i.e. node with layer data + Selection.add(node) ## Delete all nodes containing mesh data in the scene. @@ -1032,7 +1039,7 @@ class CuraApplication(QtApplication): nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: + if not issubclass(type(node), SceneNode): continue if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. @@ -1054,7 +1061,7 @@ class CuraApplication(QtApplication): Logger.log("i", "Resetting all scene translations") nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: + if not issubclass(type(node), SceneNode): continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. @@ -1082,13 +1089,13 @@ class CuraApplication(QtApplication): Logger.log("i", "Resetting all scene transformations") nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: + if not issubclass(type(node), 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(): + if not node.callDecoration("isSliceable"): continue # i.e. node with layer data nodes.append(node) @@ -1109,7 +1116,27 @@ class CuraApplication(QtApplication): def arrangeObjectsToAllBuildPlates(self): nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: + if not issubclass(type(node), 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.callDecoration("isSliceable"): + continue # i.e. node with layer data + # 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() + self.setActiveBuildPlate(0) + + # Single build plate + @pyqtSlot() + def arrangeAll(self): + nodes = [] + for node in DepthFirstIterator(self.getController().getScene().getRoot()): + if not issubclass(type(node), SceneNode): continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. @@ -1117,24 +1144,7 @@ class CuraApplication(QtApplication): 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 - # 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(): + if not node.callDecoration("isSliceable"): continue # i.e. node with layer data if node.callDecoration("getBuildPlateNumber") == self._active_build_plate: # Skip nodes that are too big @@ -1150,7 +1160,7 @@ class CuraApplication(QtApplication): # What nodes are on the build plate and are not being moved fixed_nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode: + if not issubclass(type(node), SceneNode): continue if not node.getMeshData() and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. @@ -1158,6 +1168,8 @@ class CuraApplication(QtApplication): 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 not node.callDecoration("isSliceable"): + continue # i.e. node with layer data if node in nodes: # exclude selected node from fixed_nodes continue fixed_nodes.append(node) @@ -1176,7 +1188,7 @@ class CuraApplication(QtApplication): Logger.log("i", "Reloading all loaded mesh data.") nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if type(node) is not SceneNode or not node.getMeshData(): + if not issubclass(type(node), SceneNode) or not node.getMeshData(): continue nodes.append(node) @@ -1267,7 +1279,7 @@ class CuraApplication(QtApplication): @pyqtSlot() def groupSelected(self): # Create a group-node - group_node = SceneNode() + group_node = CuraSceneNode() group_decorator = GroupDecorator() group_node.addDecorator(group_decorator) group_node.addDecorator(ConvexHullDecorator()) @@ -1413,11 +1425,15 @@ class CuraApplication(QtApplication): min_offset = 8 self.fileLoaded.emit(filename) + arrange_objects_on_load = Preferences.getInstance().getValue("cura/arrange_objects_on_load") + target_build_plate = self.activeBuildPlate if arrange_objects_on_load else -1 + + for original_node in nodes: + node = CuraSceneNode() # We want our own CuraSceneNode + node.setMeshData(original_node.getMeshData()) - for node in nodes: node.setSelectable(True) node.setName(os.path.basename(filename)) - node.addDecorator(BuildPlateDecorator()) extension = os.path.splitext(filename)[1] if extension.lower() in self._non_sliceable_extensions: @@ -1442,20 +1458,23 @@ class CuraApplication(QtApplication): if not child.getDecorator(ConvexHullDecorator): child.addDecorator(ConvexHullDecorator()) - if node.callDecoration("isSliceable"): - # Only check position if it's not already blatantly obvious that it won't fit. - if 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) + if arrange_objects_on_load: + if node.callDecoration("isSliceable"): + # Only check position if it's not already blatantly obvious that it won't fit. + if 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) - # 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 + # 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 - node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10) + # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher + node, _ = arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10) + + node.addDecorator(BuildPlateDecorator(target_build_plate)) op = AddSceneNodeOperation(node, scene.getRoot()) op.push() @@ -1494,6 +1513,8 @@ class CuraApplication(QtApplication): #### research - hacky place for these kind of thing @pyqtSlot(int) def setActiveBuildPlate(self, nr): + if nr == self._active_build_plate: + return Logger.log("d", "Select build plate: %s" % nr) self._active_build_plate = nr diff --git a/cura/ObjectManager.py b/cura/ObjectManager.py index fc6d343252..a148c05f28 100644 --- a/cura/ObjectManager.py +++ b/cura/ObjectManager.py @@ -7,23 +7,35 @@ from UM.Scene.SceneNode import SceneNode from UM.Scene.Selection import Selection from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QApplication +#from cura.Scene.CuraSceneNode import CuraSceneNode +from UM.Preferences import Preferences class ObjectManager(ListModel): def __init__(self): super().__init__() self._last_selected_index = 0 - Application.getInstance().getController().getScene().sceneChanged.connect(self._update) + Application.getInstance().getController().getScene().sceneChanged.connect(self._update_scene_changed) + Preferences.getInstance().preferenceChanged.connect(self._update) + Application.getInstance().activeBuildPlateChanged.connect(self._update) def _update(self, *args): nodes = [] + filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate") + active_build_plate_number = Application.getInstance().activeBuildPlate for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): - if type(node) is not SceneNode or (not node.getMeshData() and not node.callDecoration("getLayerData")): + if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")): + continue + if not node.callDecoration("isSliceable"): + continue + node_build_plate_number = node.callDecoration("getBuildPlateNumber") + if filter_current_build_plate and node_build_plate_number != active_build_plate_number: continue nodes.append({ "name": node.getName(), "isSelected": Selection.isSelected(node), - "buildPlateNumber": node.callDecoration("getBuildPlateNumber"), + "isOutsideBuildArea": node.isOutsideBuildArea(), + "buildPlateNumber": node_build_plate_number, "node": node }) nodes = sorted(nodes, key=lambda n: n["name"]) @@ -31,6 +43,12 @@ class ObjectManager(ListModel): self.itemsChanged.emit() + def _update_scene_changed(self, *args): + # if args and type(args[0]) is not CuraSceneNode: + # Logger.log("d", " ascdf %s", args) + # return + self._update(*args) + ## Either select or deselect an item @pyqtSlot(int) def changeSelection(self, index): @@ -63,6 +81,11 @@ class ObjectManager(ListModel): self._last_selected_index = index + # testing + for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): + if node.callDecoration("getLayerData"): + Logger.log("d", " ##### NODE: %s", node) + @staticmethod def createObjectManager(): return ObjectManager() diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index b0a14e41f4..2c886c7444 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -6,11 +6,14 @@ from UM.Logger import Logger class BuildPlateDecorator(SceneNodeDecorator): def __init__(self, build_plate_number = -1): super().__init__() + self._build_plate_number = None + self._previous_build_plate_number = None 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._previous_build_plate_number = self._build_plate_number self._build_plate_number = nr if self._node and self._node.callDecoration("isGroup"): for child in self._node.getChildren(): @@ -19,5 +22,9 @@ class BuildPlateDecorator(SceneNodeDecorator): def getBuildPlateNumber(self): return self._build_plate_number + # Used to determine from what build plate the node moved. + def getPreviousBuildPlateNumber(self): + return self._previous_build_plate_number + def __deepcopy__(self, memo): return BuildPlateDecorator() diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py new file mode 100644 index 0000000000..ccec76b53d --- /dev/null +++ b/cura/Scene/CuraSceneNode.py @@ -0,0 +1,40 @@ +from UM.Application import Application +from UM.Logger import Logger +from UM.Scene.SceneNode import SceneNode +from copy import deepcopy + + +## Scene nodes that are models are only seen when selecting the corresponding build plate +# Note that many other nodes can just be UM SceneNode objects. +class CuraSceneNode(SceneNode): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._outside_buildarea = True + + def setOutsideBuildArea(self, new_value): + self._outside_buildarea = new_value + + def isOutsideBuildArea(self): + return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0 + + def isVisible(self): + return super().isVisible() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate + + def isSelectable(self) -> bool: + return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate + + ## Taken from SceneNode, but replaced SceneNode with CuraSceneNode + def __deepcopy__(self, memo): + copy = CuraSceneNode() + copy.setTransformation(self.getLocalTransformation()) + copy.setMeshData(self._mesh_data) + copy.setVisible(deepcopy(self._visible, memo)) + copy._selectable = deepcopy(self._selectable, memo) + copy._name = deepcopy(self._name, memo) + for decorator in self._decorators: + copy.addDecorator(deepcopy(decorator, memo)) + + for child in self._children: + copy.addChild(deepcopy(child, memo)) + self.calculateBoundingBoxMesh() + return copy diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index a34bf771d7..8c4ef9d1ae 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -15,7 +15,8 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator from UM.Application import Application from cura.Settings.ExtruderManager import ExtruderManager from cura.QualityManager import QualityManager -from UM.Scene.SceneNode import SceneNode +#from UM.Scene.SceneNode import SceneNode +from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.ZOffsetDecorator import ZOffsetDecorator diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 914aa1dee0..67d3fe8c42 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -69,9 +69,10 @@ class CuraEngineBackend(QObject, Backend): # Workaround to disable layer view processing if layer view is not active. self._layer_view_active = False Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) + Application.getInstance().activeBuildPlateChanged.connect(self._onActiveViewChanged) self._onActiveViewChanged() self._stored_layer_data = [] - self._stored_optimized_layer_data = [] + self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob self._scene = Application.getInstance().getController().getScene() self._scene.sceneChanged.connect(self._onSceneChanged) @@ -104,12 +105,14 @@ class CuraEngineBackend(QObject, Backend): self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage self._start_slice_job = None + self._start_slice_job_build_plate = None self._slicing = False # Are we currently slicing? self._restart = False # Back-end is currently restarting? self._tool_active = False # If a tool is active, some tasks do not have to do anything self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness. self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers. - self._need_slicing = False + # self._need_slicing = False + self._build_plates_to_be_sliced = [] # what needs slicing? self._engine_is_fresh = True # Is the newly started engine used before or not? self._backend_log_max_lines = 20000 # Maximum number of lines to buffer @@ -189,8 +192,9 @@ class CuraEngineBackend(QObject, Backend): ## Perform a slice of the scene. def slice(self): + Logger.log("d", "starting to slice again!") self._slice_start_time = time() - if not self._need_slicing: + if not self._build_plates_to_be_sliced: self.processingProgress.emit(1.0) self.backendStateChange.emit(BackendState.Done) Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.") @@ -199,7 +203,6 @@ class CuraEngineBackend(QObject, Backend): Application.getInstance().getPrintInformation().setToZeroPrintInformation() self._stored_layer_data = [] - self._stored_optimized_layer_data = [] if self._process is None: self._createSocket() @@ -215,6 +218,9 @@ class CuraEngineBackend(QObject, Backend): slice_message = self._socket.createMessage("cura.proto.Slice") self._start_slice_job = StartSliceJob.StartSliceJob(slice_message) + self._start_slice_job_build_plate = self._build_plates_to_be_sliced.pop(0) + self._stored_optimized_layer_data[self._start_slice_job_build_plate] = [] + self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate) self._start_slice_job.start() self._start_slice_job.finished.connect(self._onStartSliceCompleted) @@ -223,7 +229,8 @@ class CuraEngineBackend(QObject, Backend): def _terminate(self): self._slicing = False self._stored_layer_data = [] - self._stored_optimized_layer_data = [] + if self._start_slice_job_build_plate in self._stored_optimized_layer_data: + del self._stored_optimized_layer_data[self._start_slice_job_build_plate] if self._start_slice_job is not None: self._start_slice_job.cancel() @@ -315,10 +322,13 @@ class CuraEngineBackend(QObject, Backend): self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() - self.backendStateChange.emit(BackendState.Error) + #self.backendStateChange.emit(BackendState.Error) else: - self.backendStateChange.emit(BackendState.NotStarted) + #self.backendStateChange.emit(BackendState.NotStarted) + pass + self._invokeSlice() return + # Preparation completed, send it to the backend. self._socket.sendMessage(job.getSliceMessage()) @@ -360,27 +370,34 @@ class CuraEngineBackend(QObject, Backend): # # \param source The scene node that was changed. def _onSceneChanged(self, source): - if type(source) is not SceneNode: + Logger.log("d", " ##### scene changed: %s", source) + if not issubclass(type(source), SceneNode): return root_scene_nodes_changed = False + build_plates_changed = set() if source == self._scene.getRoot(): num_objects = 0 for node in DepthFirstIterator(self._scene.getRoot()): # Only count sliceable objects if node.callDecoration("isSliceable"): num_objects += 1 + build_plates_changed.add(node.callDecoration("getBuildPlateNumber")) + build_plates_changed.add(node.callDecoration("getPreviousBuildPlateNumber")) if num_objects != self._last_num_objects: self._last_num_objects = num_objects root_scene_nodes_changed = True - else: - return + # else: + # return # ?? + build_plates_changed.discard(None) + build_plates_changed.discard(-1) # object not on build plate + Logger.log("d", " #### build plates changed: %s", build_plates_changed) - if not source.callDecoration("isGroup") and not root_scene_nodes_changed: - if source.getMeshData() is None: - return - if source.getMeshData().getVertices() is None: - return + # if not source.callDecoration("isGroup") and not root_scene_nodes_changed: + # if source.getMeshData() is None: + # return + # if source.getMeshData().getVertices() is None: + # return if self._tool_active: # do it later, each source only has to be done once @@ -388,9 +405,24 @@ class CuraEngineBackend(QObject, Backend): self._postponed_scene_change_sources.append(source) return - self.needsSlicing() - self.stopSlicing() - self._onChanged() + if build_plates_changed: + Logger.log("d", " going to reslice") + self.stopSlicing() + for build_plate_number in build_plates_changed: + if build_plate_number not in self._build_plates_to_be_sliced: + self._build_plates_to_be_sliced.append(build_plate_number) + self.processingProgress.emit(0.0) + self.backendStateChange.emit(BackendState.NotStarted) + if not self._use_timer: + # With manually having to slice, we want to clear the old invalid layer data. + self._clearLayerData(build_plates_changed) + + self._invokeSlice() + + # #self.needsSlicing() + # self.stopSlicing() + # #self._onChanged() + # self._invokeSlice() ## Called when an error occurs in the socket connection towards the engine. # @@ -410,16 +442,24 @@ class CuraEngineBackend(QObject, Backend): Logger.log("w", "A socket error caused the connection to be reset") ## Remove old layer data (if any) - def _clearLayerData(self): + def _clearLayerData(self, build_plate_numbers = set()): for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): - node.getParent().removeChild(node) - break + if node.callDecoration("getBuildPlateNumber") in build_plate_numbers or not build_plate_numbers: + node.getParent().removeChild(node) - ## Convenient function: set need_slicing, emit state and clear layer data + def markSliceAll(self): + if 0 not in self._build_plates_to_be_sliced: + self._build_plates_to_be_sliced.append(0) + if 1 not in self._build_plates_to_be_sliced: + self._build_plates_to_be_sliced.append(1) + if 2 not in self._build_plates_to_be_sliced: + self._build_plates_to_be_sliced.append(2) + + ## Convenient function: mark everything to slice, emit state and clear layer data def needsSlicing(self): self.stopSlicing() - self._need_slicing = True + self.markSliceAll() self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) if not self._use_timer: @@ -441,7 +481,7 @@ class CuraEngineBackend(QObject, Backend): def _onStackErrorCheckFinished(self): self._is_error_check_scheduled = False - if not self._slicing and self._need_slicing: + if not self._slicing and self._build_plates_to_be_sliced: #self._need_slicing: self.needsSlicing() self._onChanged() @@ -455,7 +495,7 @@ class CuraEngineBackend(QObject, Backend): # # \param message The protobuf message containing sliced layer data. def _onOptimizedLayerMessage(self, message): - self._stored_optimized_layer_data.append(message) + self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message) ## Called when a progress message is received from the engine. # @@ -464,6 +504,16 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(message.amount) self.backendStateChange.emit(BackendState.Processing) + # testing + def _invokeSlice(self): + if self._use_timer: + # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, + # otherwise business as usual + if self._is_error_check_scheduled: + self._change_timer.stop() + else: + self._change_timer.start() + ## Called when the engine sends a message that slicing is finished. # # \param message The protobuf message signalling that slicing is finished. @@ -481,13 +531,20 @@ class CuraEngineBackend(QObject, Backend): self._scene.gcode_list[self._scene.gcode_list.index(line)] = replaced self._slicing = False - self._need_slicing = False + #self._need_slicing = False Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time ) - if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()): - self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data) - self._process_layers_job.finished.connect(self._onProcessLayersFinished) - self._process_layers_job.start() - self._stored_optimized_layer_data = [] + + # See if we need to process the sliced layers job. + active_build_plate = Application.getInstance().activeBuildPlate + if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate: + self._startProcessSlicedLayersJob(active_build_plate) + self._start_slice_job_build_plate = None + + Logger.log("d", "See if there is more to slice...") + # Somehow this results in an Arcus Error + # self.slice() + # Testing call slice again, allow backend to restart by using the timer + self._invokeSlice() ## Called when a g-code message is received from the engine. # @@ -584,19 +641,26 @@ class CuraEngineBackend(QObject, Backend): source = self._postponed_scene_change_sources.pop(0) self._onSceneChanged(source) + def _startProcessSlicedLayersJob(self, build_plate_number): + self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number]) + self._process_layers_job.setBuildPlate(build_plate_number) + self._process_layers_job.finished.connect(self._onProcessLayersFinished) + self._process_layers_job.start() + del self._stored_optimized_layer_data[build_plate_number] + ## Called when the user changes the active view mode. def _onActiveViewChanged(self): - if Application.getInstance().getController().getActiveView(): - view = Application.getInstance().getController().getActiveView() + application = Application.getInstance() + view = application.getController().getActiveView() + if view: + active_build_plate = application.activeBuildPlate if view.getPluginId() == "LayerView": # If switching to layer view, we should process the layers if that hasn't been done yet. self._layer_view_active = True # There is data and we're not slicing at the moment # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment. - if self._stored_optimized_layer_data and not self._slicing: - self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data) - self._process_layers_job.finished.connect(self._onProcessLayersFinished) - self._process_layers_job.start() - self._stored_optimized_layer_data = [] + # TODO: what build plate I am slicing + if active_build_plate in self._stored_optimized_layer_data and not self._slicing: + self._startProcessSlicedLayersJob(active_build_plate) else: self._layer_view_active = False diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index a352564bc2..1e56f2dd35 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -17,10 +17,12 @@ from UM.Logger import Logger from UM.Math.Vector import Vector +from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.Settings.ExtruderManager import ExtruderManager from cura import LayerDataBuilder from cura import LayerDataDecorator from cura import LayerPolygon +# from cura.Scene.CuraSceneNode import CuraSceneNode import numpy from time import time @@ -49,6 +51,7 @@ class ProcessSlicedLayersJob(Job): self._scene = Application.getInstance().getController().getScene() self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1) self._abort_requested = False + self._build_plate_number = None ## Aborts the processing of layers. # @@ -59,7 +62,11 @@ class ProcessSlicedLayersJob(Job): def abort(self): self._abort_requested = True + def setBuildPlate(self, new_value): + self._build_plate_number = new_value + def run(self): + Logger.log("d", "########## Processing new layer for [%s]..." % self._build_plate_number) start_time = time() if Application.getInstance().getController().getActiveView().getPluginId() == "LayerView": self._progress_message.show() @@ -72,16 +79,18 @@ class ProcessSlicedLayersJob(Job): Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) new_node = SceneNode() + new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) - ## Remove old layer data (if any) - for node in DepthFirstIterator(self._scene.getRoot()): - if node.callDecoration("getLayerData"): - node.getParent().removeChild(node) - break - if self._abort_requested: - if self._progress_message: - self._progress_message.hide() - return + # ## Remove old layer data (if any) + # for node in DepthFirstIterator(self._scene.getRoot()): + # if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number: + # Logger.log("d", " # Removing: %s", node) + # node.getParent().removeChild(node) + # #break + # if self._abort_requested: + # if self._progress_message: + # self._progress_message.hide() + # return # Force garbage collection. # For some reason, Python has a tendency to keep the layer data diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 607914f5c5..f6abe94702 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -10,12 +10,13 @@ from UM.Job import Job from UM.Application import Application from UM.Logger import Logger -from UM.Scene.SceneNode import SceneNode +#from UM.Scene.SceneNode import SceneNode from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Settings.Validator import ValidatorState from UM.Settings.SettingRelation import RelationType +from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode from cura.OneAtATimeIterator import OneAtATimeIterator from cura.Settings.ExtruderManager import ExtruderManager @@ -58,10 +59,14 @@ class StartSliceJob(Job): self._scene = Application.getInstance().getController().getScene() self._slice_message = slice_message self._is_cancelled = False + self._build_plate_number = None def getSliceMessage(self): return self._slice_message + def setBuildPlate(self, build_plate_number): + self._build_plate_number = build_plate_number + ## Check if a stack has any errors. ## returns true if it has errors, false otherwise. def _checkStackForErrors(self, stack): @@ -78,6 +83,10 @@ class StartSliceJob(Job): ## Runs the job that initiates the slicing. def run(self): + if self._build_plate_number is None: + self.setResult(StartJobResult.Error) + return + stack = Application.getInstance().getGlobalContainerStack() if not stack: self.setResult(StartJobResult.Error) @@ -141,14 +150,12 @@ class StartSliceJob(Job): for node in DepthFirstIterator(self._scene.getRoot()): if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None: - # temp hack to filter on build plate 0 - if (node.callDecoration("getBuildPlateNumber") == 0): - - if not getattr(node, "_outside_buildarea", False)\ - or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)): + if (node.callDecoration("getBuildPlateNumber") == self._build_plate_number): + if not getattr(node, "_outside_buildarea", False) or (node.callDecoration("getStack") and any(node.callDecoration("getStack").getProperty(setting, "value") for setting in self._not_printed_mesh_settings)): temp_list.append(node) Job.yieldThread() + Logger.log("d", " objects to be sliced: %s", temp_list) if temp_list: object_groups.append(temp_list) diff --git a/plugins/ImageReader/ImageReader.py b/plugins/ImageReader/ImageReader.py index b419c0b496..2529abf2d8 100644 --- a/plugins/ImageReader/ImageReader.py +++ b/plugins/ImageReader/ImageReader.py @@ -8,12 +8,14 @@ from PyQt5.QtCore import Qt from UM.Mesh.MeshReader import MeshReader from UM.Mesh.MeshBuilder import MeshBuilder -from UM.Scene.SceneNode import SceneNode +#from UM.Scene.SceneNode import SceneNode from UM.Math.Vector import Vector from UM.Job import Job from UM.Logger import Logger from .ImageReaderUI import ImageReaderUI +from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode + class ImageReader(MeshReader): def __init__(self): diff --git a/plugins/LayerView/LayerPass.py b/plugins/LayerView/LayerPass.py index 963c8c75c8..2b0e82d4b3 100755 --- a/plugins/LayerView/LayerPass.py +++ b/plugins/LayerView/LayerPass.py @@ -66,13 +66,14 @@ class LayerPass(RenderPass): self.bind() tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay) + active_build_plate = Application.getInstance().activeBuildPlate for node in DepthFirstIterator(self._scene.getRoot()): if isinstance(node, ToolHandle): tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh()) - elif isinstance(node, SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible(): + elif issubclass(type(node), SceneNode) and (node.getMeshData() or node.callDecoration("isBlockSlicing")) and node.isVisible() and node.callDecoration("getBuildPlateNumber") == active_build_plate: layer_data = node.callDecoration("getLayerData") if not layer_data: continue diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index d37fbb7c9d..699cec23f5 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -75,7 +75,7 @@ class SolidView(View): for node in DepthFirstIterator(scene.getRoot()): if not node.render(renderer): - if node.getMeshData() and node.isVisible() and (node.callDecoration("getBuildPlateNumber") == activeBuildPlateNumber): + if node.getMeshData() and node.isVisible(): # and (node.callDecoration("getBuildPlateNumber") == activeBuildPlateNumber): uniforms = {} shade_factor = 1.0 diff --git a/plugins/X3DReader/X3DReader.py b/plugins/X3DReader/X3DReader.py index e4a59dcdaa..a0d530c78c 100644 --- a/plugins/X3DReader/X3DReader.py +++ b/plugins/X3DReader/X3DReader.py @@ -11,7 +11,8 @@ from UM.Math.Matrix import Matrix from UM.Math.Vector import Vector from UM.Mesh.MeshBuilder import MeshBuilder from UM.Mesh.MeshReader import MeshReader -from UM.Scene.SceneNode import SceneNode +#from UM.Scene.SceneNode import SceneNode +from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode MYPY = False try: @@ -19,63 +20,63 @@ try: import xml.etree.cElementTree as ET except ImportError: import xml.etree.ElementTree as ET - + # TODO: preserve the structure of scenes that contain several objects -# Use CADPart, for example, to distinguish between separate objects - +# Use CADPart, for example, to distinguish between separate objects + DEFAULT_SUBDIV = 16 # Default subdivision factor for spheres, cones, and cylinders EPSILON = 0.000001 class Shape: - + # Expects verts in MeshBuilder-ready format, as a n by 3 mdarray # with vertices stored in rows def __init__(self, verts, faces, index_base, name): self.verts = verts self.faces = faces # Those are here for debugging purposes only - self.index_base = index_base + self.index_base = index_base self.name = name - + class X3DReader(MeshReader): def __init__(self): super().__init__() self._supported_extensions = [".x3d"] self._namespaces = {} - + # Main entry point # Reads the file, returns a SceneNode (possibly with nested ones), or None def read(self, file_name): try: self.defs = {} self.shapes = [] - + tree = ET.parse(file_name) xml_root = tree.getroot() - + if xml_root.tag != "X3D": return None - scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters + scale = 1000 # Default X3D unit it one meter, while Cura's is one millimeters if xml_root[0].tag == "head": for head_node in xml_root[0]: if head_node.tag == "unit" and head_node.attrib.get("category") == "length": scale *= float(head_node.attrib["conversionFactor"]) - break + break xml_scene = xml_root[1] else: xml_scene = xml_root[0] - + if xml_scene.tag != "Scene": return None - + self.transform = Matrix() self.transform.setByScaleFactor(scale) self.index_base = 0 - + # Traverse the scene tree, populate the shapes list self.processChildNodes(xml_scene) - + if self.shapes: builder = MeshBuilder() builder.setVertices(numpy.concatenate([shape.verts for shape in self.shapes])) @@ -95,20 +96,20 @@ class X3DReader(MeshReader): else: return None - + except Exception: Logger.logException("e", "Exception in X3D reader") return None return node - + # ------------------------- XML tree traversal - + def processNode(self, xml_node): xml_node = self.resolveDefUse(xml_node) if xml_node is None: return - + tag = xml_node.tag if tag in ("Group", "StaticGroup", "CADAssembly", "CADFace", "CADLayer", "Collision"): self.processChildNodes(xml_node) @@ -120,8 +121,8 @@ class X3DReader(MeshReader): self.processTransform(xml_node) elif tag == "Shape": self.processShape(xml_node) - - + + def processShape(self, xml_node): # Find the geometry and the appearance inside the Shape geometry = appearance = None @@ -130,21 +131,21 @@ class X3DReader(MeshReader): appearance = self.resolveDefUse(sub_node) elif sub_node.tag in self.geometry_importers and not geometry: geometry = self.resolveDefUse(sub_node) - - # TODO: appearance is completely ignored. At least apply the material color... + + # TODO: appearance is completely ignored. At least apply the material color... if not geometry is None: try: - self.verts = self.faces = [] # Safeguard + self.verts = self.faces = [] # Safeguard self.geometry_importers[geometry.tag](self, geometry) m = self.transform.getData() verts = m.dot(self.verts)[:3].transpose() - + self.shapes.append(Shape(verts, self.faces, self.index_base, geometry.tag)) self.index_base += len(verts) - + except Exception: Logger.logException("e", "Exception in X3D reader while reading %s", geometry.tag) - + # Returns the referenced node if the node has USE, the same node otherwise. # May return None is USE points at a nonexistent node # In X3DOM, when both DEF and USE are in the same node, DEF is ignored. @@ -155,34 +156,34 @@ class X3DReader(MeshReader): if USE: return self.defs.get(USE, None) - DEF = node.attrib.get("DEF") + DEF = node.attrib.get("DEF") if DEF: - self.defs[DEF] = node + self.defs[DEF] = node return node - + def processChildNodes(self, node): for c in node: self.processNode(c) Job.yieldThread() - + # Since this is a grouping node, will recurse down the tree. # According to the spec, the final transform matrix is: # T * C * R * SR * S * -SR * -C # Where SR corresponds to the rotation matrix to scaleOrientation - # C and SR are rather exotic. S, slightly less so. + # C and SR are rather exotic. S, slightly less so. def processTransform(self, node): rot = readRotation(node, "rotation", (0, 0, 1, 0)) # (angle, axisVactor) tuple trans = readVector(node, "translation", (0, 0, 0)) # Vector scale = readVector(node, "scale", (1, 1, 1)) # Vector center = readVector(node, "center", (0, 0, 0)) # Vector scale_orient = readRotation(node, "scaleOrientation", (0, 0, 1, 0)) # (angle, axisVactor) tuple - - # Store the previous transform; in Cura, the default matrix multiplication is in place + + # Store the previous transform; in Cura, the default matrix multiplication is in place prev = Matrix(self.transform.getData()) # It's deep copy, I've checked - + # The rest of transform manipulation will be applied in place got_center = (center.x != 0 or center.y != 0 or center.z != 0) - + T = self.transform if trans.x != 0 or trans.y != 0 or trans.z !=0: T.translate(trans) @@ -202,13 +203,13 @@ class X3DReader(MeshReader): T.rotateByAxis(-scale_orient[0], scale_orient[1]) if got_center: T.translate(-center) - + self.processChildNodes(node) self.transform = prev - + # ------------------------- Geometry importers # They are supposed to fill the self.verts and self.faces arrays, the caller will do the rest - + # Primitives def processGeometryBox(self, node): @@ -228,14 +229,14 @@ class X3DReader(MeshReader): self.addVertex(-dx, -dy, dz) self.addVertex(-dx, -dy, -dz) self.addVertex(dx, -dy, -dz) - + self.addQuad(0, 1, 2, 3) # +y self.addQuad(4, 0, 3, 7) # +x self.addQuad(7, 3, 2, 6) # -z self.addQuad(6, 2, 1, 5) # -x self.addQuad(5, 1, 0, 4) # +z self.addQuad(7, 6, 5, 4) # -y - + # The sphere is subdivided into nr rings and ns segments def processGeometrySphere(self, node): r = readFloat(node, "radius", 0.5) @@ -247,16 +248,16 @@ class X3DReader(MeshReader): (nr, ns) = subdiv else: nr = ns = DEFAULT_SUBDIV - + lau = pi / nr # Unit angle of latitude (rings) for the given tesselation lou = 2 * pi / ns # Unit angle of longitude (segments) - + self.reserveFaceAndVertexCount(ns*(nr*2 - 2), 2 + (nr - 1)*ns) - + # +y and -y poles self.addVertex(0, r, 0) self.addVertex(0, -r, 0) - + # The non-polar vertices go from x=0, negative z plane counterclockwise - # to -x, to +z, to +x, back to -z for ring in range(1, nr): @@ -264,12 +265,12 @@ class X3DReader(MeshReader): self.addVertex(-r*sin(lou * seg) * sin(lau * ring), r*cos(lau * ring), -r*cos(lou * seg) * sin(lau * ring)) - + vb = 2 + (nr - 2) * ns # First vertex index for the bottom cap - + # Faces go in order: top cap, sides, bottom cap. # Sides go by ring then by segment. - + # Caps # Top cap face vertices go in order: down right up # (starting from +y pole) @@ -277,7 +278,7 @@ class X3DReader(MeshReader): for seg in range(ns): self.addTri(0, seg + 2, (seg + 1) % ns + 2) self.addTri(1, vb + (seg + 1) % ns, vb + seg) - + # Sides # Side face vertices go in order: down right upleft, downright up left for ring in range(nr - 2): @@ -288,24 +289,24 @@ class X3DReader(MeshReader): for seg in range(ns): nseg = (seg + 1) % ns self.addQuad(tvb + seg, bvb + seg, bvb + nseg, tvb + nseg) - + def processGeometryCone(self, node): r = readFloat(node, "bottomRadius", 1) height = readFloat(node, "height", 2) bottom = readBoolean(node, "bottom", True) side = readBoolean(node, "side", True) n = readInt(node, "subdivision", DEFAULT_SUBDIV) - + d = height / 2 angle = 2 * pi / n - + self.reserveFaceAndVertexCount((n if side else 0) + (n-2 if bottom else 0), n+1) - + # Vertex 0 is the apex, vertices 1..n are the bottom self.addVertex(0, d, 0) for i in range(n): self.addVertex(-r * sin(angle * i), -d, -r * cos(angle * i)) - + # Side face vertices go: up down right if side: for i in range(n): @@ -313,7 +314,7 @@ class X3DReader(MeshReader): if bottom: for i in range(2, n): self.addTri(1, i, i+1) - + def processGeometryCylinder(self, node): r = readFloat(node, "radius", 1) height = readFloat(node, "height", 2) @@ -321,13 +322,13 @@ class X3DReader(MeshReader): side = readBoolean(node, "side", True) top = readBoolean(node, "top", True) n = readInt(node, "subdivision", DEFAULT_SUBDIV) - + nn = n * 2 angle = 2 * pi / n hh = height/2 - + self.reserveFaceAndVertexCount((nn if side else 0) + (n - 2 if top else 0) + (n - 2 if bottom else 0), nn) - + # The seam is at x=0, z=-r, vertices go ccw - # to pos x, to neg z, to neg x, back to neg z for i in range(n): @@ -335,18 +336,18 @@ class X3DReader(MeshReader): rc = -r * cos(angle * i) self.addVertex(rs, hh, rc) self.addVertex(rs, -hh, rc) - + if side: for i in range(n): ni = (i + 1) % n self.addQuad(ni * 2 + 1, ni * 2, i * 2, i * 2 + 1) - + for i in range(2, nn-3, 2): if top: self.addTri(0, i, i+2) if bottom: self.addTri(1, i+1, i+3) - + # Semi-primitives def processGeometryElevationGrid(self, node): @@ -356,21 +357,21 @@ class X3DReader(MeshReader): nz = readInt(node, "zDimension", 0) height = readFloatArray(node, "height", False) ccw = readBoolean(node, "ccw", True) - + if nx <= 0 or nz <= 0 or len(height) < nx*nz: return # That's weird, the wording of the standard suggests grids with zero quads are somehow valid - + self.reserveFaceAndVertexCount(2*(nx-1)*(nz-1), nx*nz) - + for z in range(nz): for x in range(nx): self.addVertex(x * dx, height[z*nx + x], z * dz) - + for z in range(1, nz): for x in range(1, nx): self.addTriFlip((z - 1)*nx + x - 1, z*nx + x, (z - 1)*nx + x, ccw) self.addTriFlip((z - 1)*nx + x - 1, z*nx + x - 1, z*nx + x, ccw) - + def processGeometryExtrusion(self, node): ccw = readBoolean(node, "ccw", True) begin_cap = readBoolean(node, "beginCap", True) @@ -384,23 +385,23 @@ class X3DReader(MeshReader): # This converts X3D's axis/angle rotation to a 3x3 numpy matrix def toRotationMatrix(rot): (x, y, z) = rot[:3] - a = rot[3] + a = rot[3] s = sin(a) c = cos(a) t = 1-c return numpy.array(( (x * x * t + c, x * y * t - z*s, x * z * t + y * s), (x * y * t + z*s, y * y * t + c, y * z * t - x * s), - (x * z * t - y * s, y * z * t + x * s, z * z * t + c))) - + (x * z * t - y * s, y * z * t + x * s, z * z * t + c))) + orient = [toRotationMatrix(orient[i:i+4]) if orient[i+3] != 0 else None for i in range(0, len(orient), 4)] - + scale = readFloatArray(node, "scale", None) if scale: scale = [numpy.array(((scale[i], 0, 0), (0, 1, 0), (0, 0, scale[i+1]))) if scale[i] != 1 or scale[i+1] != 1 else None for i in range(0, len(scale), 2)] - - + + # Special treatment for the closed spine and cross section. # Let's save some memory by not creating identical but distinct vertices; # later we'll introduce conditional logic to link the last vertex with @@ -413,14 +414,14 @@ class X3DReader(MeshReader): ncf = nc if crossClosed else nc - 1 # Face count along the cross; for closed cross, it's the same as the # respective vertex count - + spine_closed = spine[0] == spine[-1] if spine_closed: spine = spine[:-1] ns = len(spine) spine = [Vector(*s) for s in spine] nsf = ns if spine_closed else ns - 1 - + # This will be used for fallback, where the current spine point joins # two collinear spine segments. No need to recheck the case of the # closed spine/last-to-first point juncture; if there's an angle there, @@ -442,7 +443,7 @@ class X3DReader(MeshReader): if v.cross(orig_y).length() > EPSILON: # Spine at angle with global y - rotate the z accordingly a = v.cross(orig_y) # Axis of rotation to get to the Z - (x, y, z) = a.normalized().getData() + (x, y, z) = a.normalized().getData() s = a.length()/v.length() c = sqrt(1-s*s) t = 1-c @@ -452,7 +453,7 @@ class X3DReader(MeshReader): (x * z * t + y * s, y * z * t - x * s, z * z * t + c))) orig_z = Vector(*m.dot(orig_z.getData())) return orig_z - + self.reserveFaceAndVertexCount(2*nsf*ncf + (nc - 2 if begin_cap else 0) + (nc - 2 if end_cap else 0), ns*nc) z = None @@ -482,151 +483,151 @@ class X3DReader(MeshReader): y = spt - sprev # If there's more than one point in the spine, z is already set. # One point in the spline is an error anyway. - + z = z.normalized() y = y.normalized() x = y.cross(z) # Already normalized m = numpy.array(((x.x, y.x, z.x), (x.y, y.y, z.y), (x.z, y.z, z.z))) - + # Columns are the unit vectors for the xz plane for the cross-section if orient: mrot = orient[i] if len(orient) > 1 else orient[0] if not mrot is None: m = m.dot(mrot) # Tested against X3DOM, the result matches, still not sure :( - + if scale: mscale = scale[i] if len(scale) > 1 else scale[0] if not mscale is None: m = m.dot(mscale) - + # First the cross-section 2-vector is scaled, # then rotated (which may make it a 3-vector), # then applied to the xz plane unit vectors - + sptv3 = numpy.array(spt.getData()[:3]) for cpt in cross: v = sptv3 + m.dot(cpt) self.addVertex(*v) - + if begin_cap: self.addFace([x for x in range(nc - 1, -1, -1)], ccw) - + # Order of edges in the face: forward along cross, forward along spine, # backward along cross, backward along spine, flipped if now ccw. # This order is assumed later in the texture coordinate assignment; # please don't change without syncing. - + for s in range(ns - 1): for c in range(ncf): self.addQuadFlip(s * nc + c, s * nc + (c + 1) % nc, (s + 1) * nc + (c + 1) % nc, (s + 1) * nc + c, ccw) - + if spine_closed: # The faces between the last and the first spine points b = (ns - 1) * nc for c in range(ncf): self.addQuadFlip(b + c, b + (c + 1) % nc, (c + 1) % nc, c, ccw) - + if end_cap: self.addFace([(ns - 1) * nc + x for x in range(0, nc)], ccw) - + # Triangle meshes # Helper for numerous nodes with a Coordinate subnode holding vertices # That all triangle meshes and IndexedFaceSet - # num_faces can be a function, in case the face count is a function of vertex count + # num_faces can be a function, in case the face count is a function of vertex count def startCoordMesh(self, node, num_faces): ccw = readBoolean(node, "ccw", True) self.readVertices(node) # This will allocate and fill the vertex array if hasattr(num_faces, "__call__"): num_faces = num_faces(self.getVertexCount()) self.reserveFaceCount(num_faces) - + return ccw - + def processGeometryIndexedTriangleSet(self, node): index = readIntArray(node, "index", []) num_faces = len(index) // 3 ccw = int(self.startCoordMesh(node, num_faces)) - + for i in range(0, num_faces*3, 3): self.addTri(index[i + 1 - ccw], index[i + ccw], index[i+2]) - + def processGeometryIndexedTriangleStripSet(self, node): strips = readIndex(node, "index") ccw = int(self.startCoordMesh(node, sum([len(strip) - 2 for strip in strips]))) - + for strip in strips: sccw = ccw # Running CCW value, reset for each strip for i in range(len(strip) - 2): self.addTri(strip[i + 1 - sccw], strip[i + sccw], strip[i+2]) sccw = 1 - sccw - + def processGeometryIndexedTriangleFanSet(self, node): fans = readIndex(node, "index") ccw = int(self.startCoordMesh(node, sum([len(fan) - 2 for fan in fans]))) - + for fan in fans: for i in range(1, len(fan) - 1): self.addTri(fan[0], fan[i + 1 - ccw], fan[i + ccw]) - + def processGeometryTriangleSet(self, node): ccw = int(self.startCoordMesh(node, lambda num_vert: num_vert // 3)) for i in range(0, self.getVertexCount(), 3): self.addTri(i + 1 - ccw, i + ccw, i+2) - + def processGeometryTriangleStripSet(self, node): strips = readIntArray(node, "stripCount", []) ccw = int(self.startCoordMesh(node, sum([n-2 for n in strips]))) - + vb = 0 for n in strips: sccw = ccw - for i in range(n-2): + for i in range(n-2): self.addTri(vb + i + 1 - sccw, vb + i + sccw, vb + i + 2) sccw = 1 - sccw vb += n - + def processGeometryTriangleFanSet(self, node): fans = readIntArray(node, "fanCount", []) ccw = int(self.startCoordMesh(node, sum([n-2 for n in fans]))) - + vb = 0 for n in fans: - for i in range(1, n-1): + for i in range(1, n-1): self.addTri(vb, vb + i + 1 - ccw, vb + i + ccw) vb += n - + # Quad geometries from the CAD module, might be relevant for printing - + def processGeometryQuadSet(self, node): ccw = self.startCoordMesh(node, lambda num_vert: 2*(num_vert // 4)) for i in range(0, self.getVertexCount(), 4): self.addQuadFlip(i, i+1, i+2, i+3, ccw) - + def processGeometryIndexedQuadSet(self, node): index = readIntArray(node, "index", []) num_quads = len(index) // 4 ccw = self.startCoordMesh(node, num_quads*2) - + for i in range(0, num_quads*4, 4): self.addQuadFlip(index[i], index[i+1], index[i+2], index[i+3], ccw) - + # 2D polygon geometries # Won't work for now, since Cura expects every mesh to have a nontrivial convex hull # The only way around that is merging meshes. - + def processGeometryDisk2D(self, node): innerRadius = readFloat(node, "innerRadius", 0) outerRadius = readFloat(node, "outerRadius", 1) n = readInt(node, "subdivision", DEFAULT_SUBDIV) - + angle = 2 * pi / n - + self.reserveFaceAndVertexCount(n*4 if innerRadius else n-2, n*2 if innerRadius else n) - + for i in range(n): s = sin(angle * i) c = cos(angle * i) @@ -635,11 +636,11 @@ class X3DReader(MeshReader): self.addVertex(innerRadius*c, innerRadius*s, 0) ni = (i+1) % n self.addQuad(2*i, 2*ni, 2*ni+1, 2*i+1) - + if not innerRadius: for i in range(2, n): self.addTri(0, i-1, i) - + def processGeometryRectangle2D(self, node): (x, y) = readFloatArray(node, "size", (2, 2)) self.reserveFaceAndVertexCount(2, 4) @@ -648,7 +649,7 @@ class X3DReader(MeshReader): self.addVertex(x/2, y/2, 0) self.addVertex(-x/2, y/2, 0) self.addQuad(0, 1, 2, 3) - + def processGeometryTriangleSet2D(self, node): verts = readFloatArray(node, "vertices", ()) num_faces = len(verts) // 6; @@ -656,25 +657,25 @@ class X3DReader(MeshReader): self.reserveFaceAndVertexCount(num_faces, num_faces * 3) for vert in verts: self.addVertex(*vert) - + # The front face is on the +Z side, so CCW is a variable for i in range(0, num_faces*3, 3): a = Vector(*verts[i+2]) - Vector(*verts[i]) b = Vector(*verts[i+1]) - Vector(*verts[i]) self.addTriFlip(i, i+1, i+2, a.x*b.y > a.y*b.x) - + # General purpose polygon mesh def processGeometryIndexedFaceSet(self, node): faces = readIndex(node, "coordIndex") ccw = self.startCoordMesh(node, sum([len(face) - 2 for face in faces])) - + for face in faces: if len(face) == 3: self.addTriFlip(face[0], face[1], face[2], ccw) elif len(face) > 3: self.addFace(face, ccw) - + geometry_importers = { "IndexedFaceSet": processGeometryIndexedFaceSet, "IndexedTriangleSet": processGeometryIndexedTriangleSet, @@ -695,7 +696,7 @@ class X3DReader(MeshReader): "Cylinder": processGeometryCylinder, "Cone": processGeometryCone } - + # Parses the Coordinate.@point field, fills the verts array. def readVertices(self, node): for c in node: @@ -704,9 +705,9 @@ class X3DReader(MeshReader): if not c is None: pt = c.attrib.get("point") if pt: - # allow the list of float values in 'point' attribute to - # be separated by commas or whitespace as per spec of - # XML encoding of X3D + # allow the list of float values in 'point' attribute to + # be separated by commas or whitespace as per spec of + # XML encoding of X3D # Ref ISO/IEC 19776-1:2015 : Section 5.1.2 co = [float(x) for vec in pt.split(',') for x in vec.split()] num_verts = len(co) // 3 @@ -715,57 +716,57 @@ class X3DReader(MeshReader): # Group by three for i in range(num_verts): self.verts[:3,i] = co[3*i:3*i+3] - + # Mesh builder helpers - + def reserveFaceAndVertexCount(self, num_faces, num_verts): # Unlike the Cura MeshBuilder, we use 4-vectors stored as columns for easier transform self.verts = numpy.zeros((4, num_verts), dtype=numpy.float32) self.verts[3,:] = numpy.ones((num_verts), dtype=numpy.float32) self.num_verts = 0 self.reserveFaceCount(num_faces) - + def reserveFaceCount(self, num_faces): self.faces = numpy.zeros((num_faces, 3), dtype=numpy.int32) self.num_faces = 0 - + def getVertexCount(self): return self.verts.shape[1] - + def addVertex(self, x, y, z): self.verts[0, self.num_verts] = x self.verts[1, self.num_verts] = y self.verts[2, self.num_verts] = z self.num_verts += 1 - + # Indices are 0-based for this shape, but they won't be zero-based in the merged mesh def addTri(self, a, b, c): self.faces[self.num_faces, 0] = self.index_base + a self.faces[self.num_faces, 1] = self.index_base + b self.faces[self.num_faces, 2] = self.index_base + c self.num_faces += 1 - + def addTriFlip(self, a, b, c, ccw): if ccw: self.addTri(a, b, c) else: self.addTri(b, a, c) - + # Needs to be convex, but not necessaily planar # Assumed ccw, cut along the ac diagonal def addQuad(self, a, b, c, d): self.addTri(a, b, c) self.addTri(c, d, a) - + def addQuadFlip(self, a, b, c, d, ccw): if ccw: self.addTri(a, b, c) self.addTri(c, d, a) else: self.addTri(a, c, b) - self.addTri(c, a, d) - - + self.addTri(c, a, d) + + # Arbitrary polygon triangulation. # Doesn't assume convexity and doesn't check the "convex" flag in the file. # Works by the "cutting of ears" algorithm: @@ -776,13 +777,13 @@ class X3DReader(MeshReader): def addFace(self, indices, ccw): # Resolve indices to coordinates for faster math face = [Vector(data=self.verts[0:3, i]) for i in indices] - + # Need a normal to the plane so that we can know which vertices form inner angles normal = findOuterNormal(face) - + if not normal: # Couldn't find an outer edge, non-planar polygon maybe? return - + # Find the vertex with the smallest inner angle and no points inside, cut off. Repeat until done n = len(face) vi = [i for i in range(n)] # We'll be using this to kick vertices from the face @@ -807,17 +808,17 @@ class X3DReader(MeshReader): if pointInsideTriangle(vx, next, prev, nextXprev): no_points_inside = False break - + if no_points_inside: max_cos = cos i_min = i - + self.addTriFlip(indices[vi[(i_min + n - 1) % n]], indices[vi[i_min]], indices[vi[(i_min + 1) % n]], ccw) vi.pop(i_min) n -= 1 self.addTriFlip(indices[vi[0]], indices[vi[1]], indices[vi[2]], ccw) - + # ------------------------------------------------------------ # X3D field parsers # ------------------------------------------------------------ @@ -844,7 +845,7 @@ def readInt(node, attr, default): if not s: return default return int(s, 0) - + def readBoolean(node, attr, default): s = node.attrib.get(attr) if not s: @@ -873,8 +874,8 @@ def readIndex(node, attr): chunk.append(v[i]) if chunk: chunks.append(chunk) - return chunks - + return chunks + # Given a face as a sequence of vectors, returns a normal to the polygon place that forms a right triple # with a vector along the polygon sequence and a vector backwards def findOuterNormal(face): @@ -894,25 +895,25 @@ def findOuterNormal(face): if rejection.dot(prev_rejection) < -EPSILON: # points on both sides of the edge - not an outer one is_outer = False break - elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability + elif rejection.length() > prev_rejection.length(): # Pick a greater rejection for numeric stability prev_rejection = rejection - + if is_outer: # Found an outer edge, prev_rejection is the rejection inside the face. Generate a normal. return edge.cross(prev_rejection) return False -# Given two *collinear* vectors a and b, returns the coefficient that takes b to a. +# Given two *collinear* vectors a and b, returns the coefficient that takes b to a. # No error handling. -# For stability, taking the ration between the biggest coordinates would be better... +# For stability, taking the ration between the biggest coordinates would be better... def ratio(a, b): if b.x > EPSILON or b.x < -EPSILON: return a.x / b.x elif b.y > EPSILON or b.y < -EPSILON: return a.y / b.y else: - return a.z / b.z - + return a.z / b.z + def pointInsideTriangle(vx, next, prev, nextXprev): vxXprev = vx.cross(prev) r = ratio(vxXprev, nextXprev) @@ -921,4 +922,4 @@ def pointInsideTriangle(vx, next, prev, nextXprev): vxXnext = vx.cross(next); s = -ratio(vxXnext, nextXprev) return s > 0 and (s + r) < 1 - + diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 03d0ce9ecd..29d8b439a8 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -409,6 +409,7 @@ UM.MainWindow { id: objectsList; visible: false; + //z: -10; anchors { top: objectsButton.top; diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index 4a7c84c41e..cde7f065fa 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -55,7 +55,7 @@ Rectangle //anchors.right: parent.right width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30 text: Cura.ObjectManager.getItem(index).name; - color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : palette.text + color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : (Cura.ObjectManager.getItem(index).isOutsideBuildArea ? palette.mid : palette.text) elide: Text.ElideRight } @@ -95,7 +95,7 @@ Rectangle topMargin: UM.Theme.getSize("default_margin").height; left: parent.left; leftMargin: UM.Theme.getSize("default_margin").height; - bottom: buildPlateSelection.top; + bottom: filterBuildPlateCheckbox.top; bottomMargin: UM.Theme.getSize("default_margin").height; } @@ -115,6 +115,25 @@ Rectangle } } + + CheckBox + { + id: filterBuildPlateCheckbox + checked: boolCheck(UM.Preferences.getValue("view/filter_current_build_plate")) + onClicked: UM.Preferences.setValue("view/filter_current_build_plate", checked) + + text: catalog.i18nc("@option:check","Filter active build plate"); + + anchors + { + left: parent.left; + topMargin: UM.Theme.getSize("default_margin").height; + bottomMargin: UM.Theme.getSize("default_margin").height; + leftMargin: UM.Theme.getSize("default_margin").height; + bottom: buildPlateSelection.top; + } + } + ListModel { id: buildPlatesModel diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index ad6c2ce050..c74fb5720d 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -304,7 +304,7 @@ UM.PreferencesPage text: catalog.i18nc("@option:check","Slice automatically"); } } - + Item { //: Spacer @@ -451,6 +451,20 @@ UM.PreferencesPage text: catalog.i18nc("@label","Opening and saving files") } + UM.TooltipArea { + width: childrenRect.width + height: childrenRect.height + text: catalog.i18nc("@info:tooltip","Should newly loaded models be arranged on the build palte?") + + CheckBox + { + id: arrangeOnLoadCheckbox + text: catalog.i18nc("@option:check","Arrange objects on load") + checked: boolCheck(UM.Preferences.getValue("cura/arrange_objects_on_load")) + onCheckedChanged: UM.Preferences.setValue("cura/arrange_objects_on_load", checked) + } + } + UM.TooltipArea { width: childrenRect.width height: childrenRect.height From c73247016912af860ce1a6a351ae33a64c09d06f Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 13 Nov 2017 13:01:58 +0100 Subject: [PATCH 06/42] CURA-4525 refined the condition when to reslice build plates; however from the layer view it (still) doesn't always show the layers --- cura/Scene/BuildPlateDecorator.py | 3 + .../CuraEngineBackend/CuraEngineBackend.py | 112 +++++++++++------- 2 files changed, 74 insertions(+), 41 deletions(-) diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index 2c886c7444..41921a120f 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -26,5 +26,8 @@ class BuildPlateDecorator(SceneNodeDecorator): def getPreviousBuildPlateNumber(self): return self._previous_build_plate_number + def removePreviousBuildPlateNumber(self): + self._previous_build_plate_number = None + def __deepcopy__(self, memo): return BuildPlateDecorator() diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 67d3fe8c42..78e1f9f8d6 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -16,6 +16,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Qt.Duration import DurationFormat from PyQt5.QtCore import QObject, pyqtSlot +from collections import defaultdict from cura.Settings.ExtruderManager import ExtruderManager from . import ProcessSlicedLayersJob from . import StartSliceJob @@ -117,7 +118,7 @@ class CuraEngineBackend(QObject, Backend): self._backend_log_max_lines = 20000 # Maximum number of lines to buffer self._error_message = None # Pop-up message that shows errors. - self._last_num_objects = 0 # Count number of objects to see if there is something changed + self._last_num_objects = defaultdict(int) # Count number of objects to see if there is something changed self._postponed_scene_change_sources = [] # scene change is postponed (by a tool) self.backendQuit.connect(self._onBackendQuit) @@ -192,17 +193,26 @@ class CuraEngineBackend(QObject, Backend): ## Perform a slice of the scene. def slice(self): - Logger.log("d", "starting to slice again!") + Logger.log("d", "starting to slice!") self._slice_start_time = time() if not self._build_plates_to_be_sliced: self.processingProgress.emit(1.0) self.backendStateChange.emit(BackendState.Done) Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.") return - if Application.getInstance().getPrintInformation(): - Application.getInstance().getPrintInformation().setToZeroPrintInformation() + + # see if we really have to slice + build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0) + num_objects = self._numObjects() + if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0: + Logger.log("d", " ## Build plate %s has 0 objects to be sliced, skipping", build_plate_to_be_sliced) + self._invokeSlice() self._stored_layer_data = [] + self._stored_optimized_layer_data[build_plate_to_be_sliced] = [] + + if Application.getInstance().getPrintInformation(): + Application.getInstance().getPrintInformation().setToZeroPrintInformation() if self._process is None: self._createSocket() @@ -218,8 +228,7 @@ class CuraEngineBackend(QObject, Backend): slice_message = self._socket.createMessage("cura.proto.Slice") self._start_slice_job = StartSliceJob.StartSliceJob(slice_message) - self._start_slice_job_build_plate = self._build_plates_to_be_sliced.pop(0) - self._stored_optimized_layer_data[self._start_slice_job_build_plate] = [] + self._start_slice_job_build_plate = build_plate_to_be_sliced self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate) self._start_slice_job.start() self._start_slice_job.finished.connect(self._onStartSliceCompleted) @@ -364,40 +373,62 @@ class CuraEngineBackend(QObject, Backend): self.disableTimer() return False + ## Return a dict with number of objects per build plate + def _numObjects(self): + num_objects = defaultdict(int) + for node in DepthFirstIterator(self._scene.getRoot()): + # Only count sliceable objects + if node.callDecoration("isSliceable"): + build_plate_number = node.callDecoration("getBuildPlateNumber") + num_objects[build_plate_number] += 1 + return num_objects + ## Listener for when the scene has changed. # # This should start a slice if the scene is now ready to slice. # # \param source The scene node that was changed. def _onSceneChanged(self, source): - Logger.log("d", " ##### scene changed: %s", source) if not issubclass(type(source), SceneNode): return - root_scene_nodes_changed = False - build_plates_changed = set() + build_plate_changed = set() + source_build_plate_number = source.callDecoration("getBuildPlateNumber") if source == self._scene.getRoot(): - num_objects = 0 + # we got the root node + num_objects = self._numObjects() + # num_objects = defaultdict(int) for node in DepthFirstIterator(self._scene.getRoot()): - # Only count sliceable objects - if node.callDecoration("isSliceable"): - num_objects += 1 - build_plates_changed.add(node.callDecoration("getBuildPlateNumber")) - build_plates_changed.add(node.callDecoration("getPreviousBuildPlateNumber")) - if num_objects != self._last_num_objects: - self._last_num_objects = num_objects - root_scene_nodes_changed = True - # else: - # return # ?? - build_plates_changed.discard(None) - build_plates_changed.discard(-1) # object not on build plate - Logger.log("d", " #### build plates changed: %s", build_plates_changed) + # # Only count sliceable objects + # if node.callDecoration("isSliceable"): + # build_plate_number = node.callDecoration("getBuildPlateNumber") + # num_objects[build_plate_number] += 1 + node.callDecoration("removePreviousBuildPlateNumber") # use the previous build plate number one time + for build_plate_number in list(self._last_num_objects.keys()) + list(num_objects.keys()): + if build_plate_number not in self._last_num_objects or num_objects[build_plate_number] != self._last_num_objects[build_plate_number]: + self._last_num_objects[build_plate_number] = num_objects[build_plate_number] + build_plate_changed.add(build_plate_number) + else: + # we got a single scenenode, how do we know if it's changed? + # build_plate_changed.add(source_build_plate_number) + # build_plate_changed.add(source.callDecoration("getPreviousBuildPlateNumber")) + # source.callDecoration("removePreviousBuildPlateNumber") # use the previous build plate number one time + if not source.callDecoration("isGroup"): + if source.getMeshData() is None: + return + if source.getMeshData().getVertices() is None: + return - # if not source.callDecoration("isGroup") and not root_scene_nodes_changed: - # if source.getMeshData() is None: - # return - # if source.getMeshData().getVertices() is None: - # return + # we got a single object and it passed all the tests of being changed + build_plate_changed.add(source_build_plate_number) + build_plate_changed.add(source.callDecoration("getPreviousBuildPlateNumber")) + source.callDecoration("removePreviousBuildPlateNumber") # use the previous build plate number one time + + build_plate_changed.discard(None) + build_plate_changed.discard(-1) # object not on build plate + if not build_plate_changed: + return + # Logger.log("d", " #### build plates changed: %s", build_plate_changed) if self._tool_active: # do it later, each source only has to be done once @@ -405,19 +436,18 @@ class CuraEngineBackend(QObject, Backend): self._postponed_scene_change_sources.append(source) return - if build_plates_changed: - Logger.log("d", " going to reslice") - self.stopSlicing() - for build_plate_number in build_plates_changed: - if build_plate_number not in self._build_plates_to_be_sliced: - self._build_plates_to_be_sliced.append(build_plate_number) - self.processingProgress.emit(0.0) - self.backendStateChange.emit(BackendState.NotStarted) - if not self._use_timer: - # With manually having to slice, we want to clear the old invalid layer data. - self._clearLayerData(build_plates_changed) + Logger.log("d", " going to reslice: %s", build_plate_changed) + self.stopSlicing() + for build_plate_number in build_plate_changed: + if build_plate_number not in self._build_plates_to_be_sliced: + self._build_plates_to_be_sliced.append(build_plate_number) + self.processingProgress.emit(0.0) + self.backendStateChange.emit(BackendState.NotStarted) + if not self._use_timer: + # With manually having to slice, we want to clear the old invalid layer data. + self._clearLayerData(build_plate_changed) - self._invokeSlice() + self._invokeSlice() # #self.needsSlicing() # self.stopSlicing() @@ -445,7 +475,7 @@ class CuraEngineBackend(QObject, Backend): def _clearLayerData(self, build_plate_numbers = set()): for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("getLayerData"): - if node.callDecoration("getBuildPlateNumber") in build_plate_numbers or not build_plate_numbers: + if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers: node.getParent().removeChild(node) def markSliceAll(self): From 040cc31079e904aa10a3519f44191d33a7ca5d5a Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 13 Nov 2017 14:25:22 +0100 Subject: [PATCH 07/42] CURA-4525 layer data viewing seems fixed, although after moving an object to a different build plate triggers the reslice only after deselecting --- cura/Scene/BuildPlateDecorator.py | 9 --- .../CuraEngineBackend/CuraEngineBackend.py | 58 +++++++++---------- .../ProcessSlicedLayersJob.py | 3 + plugins/CuraEngineBackend/StartSliceJob.py | 3 +- 4 files changed, 32 insertions(+), 41 deletions(-) diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index 41921a120f..6b4dcfc970 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -7,13 +7,11 @@ class BuildPlateDecorator(SceneNodeDecorator): def __init__(self, build_plate_number = -1): super().__init__() self._build_plate_number = None - self._previous_build_plate_number = None 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._previous_build_plate_number = self._build_plate_number self._build_plate_number = nr if self._node and self._node.callDecoration("isGroup"): for child in self._node.getChildren(): @@ -22,12 +20,5 @@ class BuildPlateDecorator(SceneNodeDecorator): def getBuildPlateNumber(self): return self._build_plate_number - # Used to determine from what build plate the node moved. - def getPreviousBuildPlateNumber(self): - return self._previous_build_plate_number - - def removePreviousBuildPlateNumber(self): - self._previous_build_plate_number = None - def __deepcopy__(self, memo): return BuildPlateDecorator() diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 78e1f9f8d6..ebe4634786 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -177,6 +177,7 @@ class CuraEngineBackend(QObject, Backend): self._createSocket() if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon. + Logger.log("d", "Aborting process layers job...") self._process_layers_job.abort() self._process_layers_job = None @@ -201,12 +202,19 @@ class CuraEngineBackend(QObject, Backend): Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.") return + if self._process_layers_job: + Logger.log("d", " ## Process layers job still busy, trying later") + self._invokeSlice() + return + # see if we really have to slice build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0) + Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced) num_objects = self._numObjects() if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0: Logger.log("d", " ## Build plate %s has 0 objects to be sliced, skipping", build_plate_to_be_sliced) self._invokeSlice() + return self._stored_layer_data = [] self._stored_optimized_layer_data[build_plate_to_be_sliced] = [] @@ -326,17 +334,18 @@ class CuraEngineBackend(QObject, Backend): else: self.backendStateChange.emit(BackendState.NotStarted) - if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice: - if Application.getInstance().platformActivity: - self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."), - title = catalog.i18nc("@info:title", "Unable to slice")) - self._error_message.show() - #self.backendStateChange.emit(BackendState.Error) - else: - #self.backendStateChange.emit(BackendState.NotStarted) - pass - self._invokeSlice() - return + # Doesn't occur anymore, is handled in slice() + # if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice: + # if Application.getInstance().platformActivity: + # self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."), + # title = catalog.i18nc("@info:title", "Unable to slice")) + # self._error_message.show() + # #self.backendStateChange.emit(BackendState.Error) + # else: + # #self.backendStateChange.emit(BackendState.NotStarted) + # pass + # self._invokeSlice() + # return # Preparation completed, send it to the backend. self._socket.sendMessage(job.getSliceMessage()) @@ -397,38 +406,24 @@ class CuraEngineBackend(QObject, Backend): if source == self._scene.getRoot(): # we got the root node num_objects = self._numObjects() - # num_objects = defaultdict(int) - for node in DepthFirstIterator(self._scene.getRoot()): - # # Only count sliceable objects - # if node.callDecoration("isSliceable"): - # build_plate_number = node.callDecoration("getBuildPlateNumber") - # num_objects[build_plate_number] += 1 - node.callDecoration("removePreviousBuildPlateNumber") # use the previous build plate number one time for build_plate_number in list(self._last_num_objects.keys()) + list(num_objects.keys()): if build_plate_number not in self._last_num_objects or num_objects[build_plate_number] != self._last_num_objects[build_plate_number]: self._last_num_objects[build_plate_number] = num_objects[build_plate_number] build_plate_changed.add(build_plate_number) else: - # we got a single scenenode, how do we know if it's changed? - # build_plate_changed.add(source_build_plate_number) - # build_plate_changed.add(source.callDecoration("getPreviousBuildPlateNumber")) - # source.callDecoration("removePreviousBuildPlateNumber") # use the previous build plate number one time + # we got a single scenenode if not source.callDecoration("isGroup"): if source.getMeshData() is None: return if source.getMeshData().getVertices() is None: return - # we got a single object and it passed all the tests of being changed build_plate_changed.add(source_build_plate_number) - build_plate_changed.add(source.callDecoration("getPreviousBuildPlateNumber")) - source.callDecoration("removePreviousBuildPlateNumber") # use the previous build plate number one time build_plate_changed.discard(None) build_plate_changed.discard(-1) # object not on build plate if not build_plate_changed: return - # Logger.log("d", " #### build plates changed: %s", build_plate_changed) if self._tool_active: # do it later, each source only has to be done once @@ -443,9 +438,9 @@ class CuraEngineBackend(QObject, Backend): self._build_plates_to_be_sliced.append(build_plate_number) self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) - if not self._use_timer: + # if not self._use_timer: # With manually having to slice, we want to clear the old invalid layer data. - self._clearLayerData(build_plate_changed) + self._clearLayerData(build_plate_changed) self._invokeSlice() @@ -568,6 +563,7 @@ class CuraEngineBackend(QObject, Backend): active_build_plate = Application.getInstance().activeBuildPlate if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate: self._startProcessSlicedLayersJob(active_build_plate) + # self._onActiveViewChanged() self._start_slice_job_build_plate = None Logger.log("d", "See if there is more to slice...") @@ -676,7 +672,6 @@ class CuraEngineBackend(QObject, Backend): self._process_layers_job.setBuildPlate(build_plate_number) self._process_layers_job.finished.connect(self._onProcessLayersFinished) self._process_layers_job.start() - del self._stored_optimized_layer_data[build_plate_number] ## Called when the user changes the active view mode. def _onActiveViewChanged(self): @@ -689,7 +684,7 @@ class CuraEngineBackend(QObject, Backend): # There is data and we're not slicing at the moment # if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment. # TODO: what build plate I am slicing - if active_build_plate in self._stored_optimized_layer_data and not self._slicing: + if active_build_plate in self._stored_optimized_layer_data and not self._slicing and not self._process_layers_job: self._startProcessSlicedLayersJob(active_build_plate) else: self._layer_view_active = False @@ -726,7 +721,10 @@ class CuraEngineBackend(QObject, Backend): self._onChanged() def _onProcessLayersFinished(self, job): + del self._stored_optimized_layer_data[job.getBuildPlate()] self._process_layers_job = None + Logger.log("d", "See if there is more to slice(2)...") + self._invokeSlice() ## Connect slice function to timer. def enableTimer(self): diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 1e56f2dd35..916cc4d914 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -65,6 +65,9 @@ class ProcessSlicedLayersJob(Job): def setBuildPlate(self, new_value): self._build_plate_number = new_value + def getBuildPlate(self): + return self._build_plate_number + def run(self): Logger.log("d", "########## Processing new layer for [%s]..." % self._build_plate_number) start_time = time() diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index f6abe94702..cf6043c6cb 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -120,7 +120,7 @@ class StartSliceJob(Job): with self._scene.getSceneLock(): # Remove old layer data. for node in DepthFirstIterator(self._scene.getRoot()): - if node.callDecoration("getLayerData"): + if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number: node.getParent().removeChild(node) break @@ -155,7 +155,6 @@ class StartSliceJob(Job): temp_list.append(node) Job.yieldThread() - Logger.log("d", " objects to be sliced: %s", temp_list) if temp_list: object_groups.append(temp_list) From 8e5e555344bb1b7e59779390358ecbd3e4ae1501 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 13 Nov 2017 16:27:15 +0100 Subject: [PATCH 08/42] CURA-4525 Send all build plate gcodes to printer at one press of the button :-) --- cura/CuraApplication.py | 19 +++++++++++- cura/GCodeListDecorator.py | 2 +- .../CuraEngineBackend/CuraEngineBackend.py | 16 ++++++---- .../NetworkClusterPrinterOutputDevice.py | 30 ++++++++++++++----- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index c1c894c735..de78f808cd 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -258,6 +258,7 @@ class CuraApplication(QtApplication): self._i18n_catalog = i18nCatalog("cura") self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity) + self.getController().getScene().sceneChanged.connect(self.updateMaxBuildPlate) # it may be a bit inefficient when changing a lot simultaneously self.getController().toolOperationStopped.connect(self._onToolOperationStopped) self.getController().contextMenuRequested.connect(self._onContextMenuRequested) @@ -1523,7 +1524,7 @@ class CuraApplication(QtApplication): @pyqtSlot() def newBuildPlate(self): Logger.log("d", "New build plate") - self._num_build_plates += 1 + #self._num_build_plates += 1 self.numBuildPlatesChanged.emit() @pyqtProperty(int, notify = numBuildPlatesChanged) @@ -1533,3 +1534,19 @@ class CuraApplication(QtApplication): @pyqtProperty(int, notify = activeBuildPlateChanged) def activeBuildPlate(self): return self._active_build_plate + + def updateMaxBuildPlate(self, source): + if not issubclass(type(source), SceneNode): + return + num_build_plates = self._calcMaxBuildPlate() + if num_build_plates != self._num_build_plates: + self._num_build_plates = num_build_plates + self.numBuildPlatesChanged.emit() + + def _calcMaxBuildPlate(self): + max_build_plate = 0 + for node in DepthFirstIterator(self.getController().getScene().getRoot()): + if node.callDecoration("isSliceable"): + build_plate_number = node.callDecoration("getBuildPlateNumber") + max_build_plate = max(build_plate_number, max_build_plate) + return max_build_plate diff --git a/cura/GCodeListDecorator.py b/cura/GCodeListDecorator.py index 5738d0a7f2..66ecf3beac 100644 --- a/cura/GCodeListDecorator.py +++ b/cura/GCodeListDecorator.py @@ -4,7 +4,7 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator class GCodeListDecorator(SceneNodeDecorator): def __init__(self): super().__init__() - self._gcode_list = [] + self._gcode_list = {} # [] def getGCodeList(self): return self._gcode_list diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index ebe4634786..f8b68e118a 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -230,7 +230,9 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) - self._scene.gcode_list = [] + if not hasattr(self._scene, "gcode_list"): + self._scene.gcode_list = {} + self._scene.gcode_list[build_plate_to_be_sliced] = [] #[] indexed by build plate number self._slicing = True self.slicingStarted.emit() @@ -370,7 +372,7 @@ class CuraEngineBackend(QObject, Backend): self.backendStateChange.emit(BackendState.Disabled) gcode_list = node.callDecoration("getGCodeList") if gcode_list is not None: - self._scene.gcode_list = gcode_list + self._scene.gcode_list[node.callDecoration("getBuildPlateNumber")] = gcode_list if self._use_timer == enable_timer: return self._use_timer @@ -546,14 +548,16 @@ class CuraEngineBackend(QObject, Backend): self.backendStateChange.emit(BackendState.Done) self.processingProgress.emit(1.0) - for line in self._scene.gcode_list: + gcode_list = self._scene.gcode_list[self._start_slice_job_build_plate] + for index, line in enumerate(gcode_list): replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601))) replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths)) replaced = replaced.replace("{filament_weight}", str(Application.getInstance().getPrintInformation().materialWeights)) replaced = replaced.replace("{filament_cost}", str(Application.getInstance().getPrintInformation().materialCosts)) replaced = replaced.replace("{jobname}", str(Application.getInstance().getPrintInformation().jobName)) - self._scene.gcode_list[self._scene.gcode_list.index(line)] = replaced + #gcode_list[gcode_list.index(line)] = replaced + gcode_list[index] = replaced self._slicing = False #self._need_slicing = False @@ -576,14 +580,14 @@ class CuraEngineBackend(QObject, Backend): # # \param message The protobuf message containing g-code, encoded as UTF-8. def _onGCodeLayerMessage(self, message): - self._scene.gcode_list.append(message.data.decode("utf-8", "replace")) + self._scene.gcode_list[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) ## Called when a g-code prefix message is received from the engine. # # \param message The protobuf message containing the g-code prefix, # encoded as UTF-8. def _onGCodePrefixMessage(self, message): - self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace")) + self._scene.gcode_list[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) ## Creates a new socket connection. def _createSocket(self): diff --git a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py index b6e94121f8..14a60a6b22 100644 --- a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py @@ -254,6 +254,10 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte self._selected_printer = self._automatic_printer # reset to default option self._request_job = [nodes, file_name, filter_by_machine, file_handler, kwargs] + # the build plates to be sent + self._job_list = list(getattr(Application.getInstance().getController().getScene(), "gcode_list").keys()) + Logger.log("d", "build plates to be sent to printer: %s", (self._job_list)) + if self._stage != OutputStage.ready: if self._error_message: self._error_message.hide() @@ -263,12 +267,14 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte self._error_message.show() return + self._add_build_plate_number = len(self._job_list) > 1 if len(self._printers) > 1: self.spawnPrintView() # Ask user how to print it. elif len(self._printers) == 1: # If there is only one printer, don't bother asking. self.selectAutomaticPrinter() self.sendPrintJob() + else: # Cluster has no printers, warn the user of this. if self._error_message: @@ -283,28 +289,34 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte @pyqtSlot() def sendPrintJob(self): nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job - require_printer_name = self._selected_printer["unique_name"] + output_build_plate_number = self._job_list.pop(0) + gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")[output_build_plate_number] self._send_gcode_start = time.time() - Logger.log("d", "Sending print job [%s] to host..." % file_name) + Logger.log("d", "Sending print job [%s] to host, build plate [%s]..." % (file_name, output_build_plate_number)) if self._stage != OutputStage.ready: Logger.log("d", "Unable to send print job as the state is %s", self._stage) raise OutputDeviceError.DeviceBusyError() self._stage = OutputStage.uploading - self._file_name = "%s.gcode.gz" % file_name + if self._add_build_plate_number: + self._file_name = "%s_%d.gcode.gz" % (file_name, output_build_plate_number) + else: + self._file_name = "%s.gcode.gz" % (file_name) self._showProgressMessage() - new_request = self._buildSendPrintJobHttpRequest(require_printer_name) + require_printer_name = self._selected_printer["unique_name"] + + new_request = self._buildSendPrintJobHttpRequest(require_printer_name, gcode) if new_request is None or self._stage != OutputStage.uploading: return self._request = new_request self._reply = self._manager.post(self._request, self._multipart) self._reply.uploadProgress.connect(self._onUploadProgress) - # See _finishedPostPrintJobRequest() + # See _finishedPrintJobPostRequest() - def _buildSendPrintJobHttpRequest(self, require_printer_name): + def _buildSendPrintJobHttpRequest(self, require_printer_name, gcode): api_url = QUrl(self._api_base_uri + "print_jobs/") request = QNetworkRequest(api_url) # Create multipart request and add the g-code. @@ -313,9 +325,8 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte # Add gcode part = QHttpPart() part.setHeader(QNetworkRequest.ContentDispositionHeader, - 'form-data; name="file"; filename="%s"' % self._file_name) + 'form-data; name="file"; filename="%s"' % (self._file_name)) - gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") compressed_gcode = self._compressGcode(gcode) if compressed_gcode is None: return None # User aborted print, so stop trying. @@ -401,6 +412,9 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte self._cleanupRequest() + if self._job_list: # start sending next job + self.sendPrintJob() + def _showRequestFailedMessage(self, reply): if reply is not None: Logger.log("w", "Unable to send print job to group {cluster_name}: {error_string} ({error})".format( From 97f61366a83a24e0fa5c5664dc513e29b70bd5de Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Mon, 13 Nov 2017 16:51:07 +0100 Subject: [PATCH 09/42] CURA-4525 fix accidently remove all scenenodes when deleteAll --- cura/CuraApplication.py | 2 +- resources/qml/ObjectsList.qml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index de78f808cd..7667d6dad8 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1040,7 +1040,7 @@ class CuraApplication(QtApplication): nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if not issubclass(type(node), SceneNode): + if type(node) not in {SceneNode, CuraSceneNode}: continue if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"): continue # Node that doesnt have a mesh and is not a group. diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index cde7f065fa..4eeb0cd568 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -54,7 +54,7 @@ Rectangle anchors.leftMargin: UM.Theme.getSize("default_margin").width //anchors.right: parent.right width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30 - text: Cura.ObjectManager.getItem(index).name; + text: Cura.ObjectManager.getItem(index) ? Cura.ObjectManager.getItem(index).name : ""; color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : (Cura.ObjectManager.getItem(index).isOutsideBuildArea ? palette.mid : palette.text) elide: Text.ElideRight } @@ -66,7 +66,7 @@ Rectangle anchors.left: nodeNameLabel.right anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.right: parent.right - text: Cura.ObjectManager.getItem(index).buildPlateNumber; + text: Cura.ObjectManager.getItem(index) ? Cura.ObjectManager.getItem(index).buildPlateNumber : 0; color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : palette.text elide: Text.ElideRight } From bd8aa8d989bb53b968ef1d114776aece8ca4bf9f Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 14 Nov 2017 14:27:46 +0100 Subject: [PATCH 10/42] CURA-4525 automatic build plate menu items using BuildPlateModel --- cura/BuildPlateModel.py | 60 +++++++++++++++++ cura/ConvexHullNode.py | 2 +- cura/CuraApplication.py | 64 ++++--------------- cura/ObjectManager.py | 15 ++--- cura/Scene/CuraSceneNode.py | 4 +- .../CuraEngineBackend/CuraEngineBackend.py | 6 +- plugins/LayerView/LayerPass.py | 2 +- plugins/SolidView/SolidView.py | 4 +- resources/qml/Cura.qml | 7 +- resources/qml/Menus/ContextMenu.qml | 28 ++++---- resources/qml/Menus/ViewMenu.qml | 41 ++++++------ resources/qml/ObjectsList.qml | 50 +++------------ 12 files changed, 131 insertions(+), 152 deletions(-) diff --git a/cura/BuildPlateModel.py b/cura/BuildPlateModel.py index 139597f9cb..35a311dc5f 100644 --- a/cura/BuildPlateModel.py +++ b/cura/BuildPlateModel.py @@ -1,2 +1,62 @@ +from UM.Qt.ListModel import ListModel +from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +from UM.Scene.SceneNode import SceneNode +from UM.Logger import Logger +from UM.Application import Application +class BuildPlateModel(ListModel): + maxBuildPlateChanged = pyqtSignal() + activeBuildPlateChanged = pyqtSignal() + + def __init__(self): + super().__init__() + Application.getInstance().getController().getScene().sceneChanged.connect(self.updateMaxBuildPlate) # it may be a bit inefficient when changing a lot simultaneously + + self._max_build_plate = 1 # default + self._active_build_plate = 0 + + @pyqtSlot(int) + def setActiveBuildPlate(self, nr): + if nr == self._active_build_plate: + return + Logger.log("d", "Select build plate: %s" % nr) + self._active_build_plate = nr + + self.activeBuildPlateChanged.emit() + + @pyqtProperty(int, notify = activeBuildPlateChanged) + def activeBuildPlate(self): + return self._active_build_plate + + ## Return the highest build plate number + @pyqtProperty(int, notify = maxBuildPlateChanged) + def maxBuildPlate(self): + return self._max_build_plate + + def updateMaxBuildPlate(self, source): + if not issubclass(type(source), SceneNode): + return + max_build_plate = self._calcMaxBuildPlate() + changed = False + if max_build_plate != self._max_build_plate: + self._max_build_plate = max_build_plate + changed = True + if changed: + self.maxBuildPlateChanged.emit() + build_plates = [{"name": "Build Plate %d" % (i + 1), "buildPlateNumber": i} for i in range(self._max_build_plate + 1)] + self.setItems(build_plates) + self.itemsChanged.emit() + + def _calcMaxBuildPlate(self): + max_build_plate = 0 + for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): + if node.callDecoration("isSliceable"): + build_plate_number = node.callDecoration("getBuildPlateNumber") + max_build_plate = max(build_plate_number, max_build_plate) + return max_build_plate + + @staticmethod + def createBuildPlateModel(): + return BuildPlateModel() diff --git a/cura/ConvexHullNode.py b/cura/ConvexHullNode.py index c6ff80670d..cec9d3d698 100644 --- a/cura/ConvexHullNode.py +++ b/cura/ConvexHullNode.py @@ -64,7 +64,7 @@ class ConvexHullNode(SceneNode): ConvexHullNode.shader.setUniformValue("u_diffuseColor", self._color) ConvexHullNode.shader.setUniformValue("u_opacity", 0.6) - if self.getParent() and self.getParent().callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate: + if self.getParent() and self.getParent().callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate: if self.getMeshData(): renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8) if self._convex_hull_head_mesh: diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 7667d6dad8..08b81d568c 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -40,7 +40,6 @@ 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.Scene.CuraSceneNode import CuraSceneNode @@ -83,6 +82,7 @@ from cura.Settings.GlobalStack import GlobalStack from cura.Settings.ExtruderStack import ExtruderStack from cura.ObjectManager import ObjectManager +from cura.BuildPlateModel import BuildPlateModel from PyQt5.QtCore import QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS from UM.FlameProfiler import pyqtSlot @@ -211,6 +211,7 @@ class CuraApplication(QtApplication): self._machine_manager = None # This is initialized on demand. self._material_manager = None self._object_manager = None + self._build_plate_model = None self._setting_inheritance_manager = None self._simple_mode_settings_manager = None @@ -258,7 +259,6 @@ class CuraApplication(QtApplication): self._i18n_catalog = i18nCatalog("cura") self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity) - self.getController().getScene().sceneChanged.connect(self.updateMaxBuildPlate) # it may be a bit inefficient when changing a lot simultaneously self.getController().toolOperationStopped.connect(self._onToolOperationStopped) self.getController().contextMenuRequested.connect(self._onContextMenuRequested) @@ -389,10 +389,6 @@ class CuraApplication(QtApplication): self._plugin_registry.addSupportedPluginExtension("curaplugin", "Cura Plugin") - # research - self._num_build_plates = 1 # default - self._active_build_plate = 0 - def _onEngineCreated(self): self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) @@ -724,8 +720,8 @@ class CuraApplication(QtApplication): qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager) - Logger.log("d", " #### going to register object manager") qmlRegisterSingletonType(ObjectManager, "Cura", 1, 2, "ObjectManager", self.getObjectManager) + qmlRegisterSingletonType(BuildPlateModel, "Cura", 1, 2, "BuildPlateModel", self.getBuildPlateModel) qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager) self.setMainQml(Resources.getPath(self.ResourceTypes.QmlFiles, "Cura.qml")) @@ -759,6 +755,11 @@ class CuraApplication(QtApplication): self._object_manager = ObjectManager.createObjectManager() return self._object_manager + def getBuildPlateModel(self, *args): + if self._build_plate_model is None: + self._build_plate_model = BuildPlateModel.createBuildPlateModel() + return self._build_plate_model + def getSettingInheritanceManager(self, *args): if self._setting_inheritance_manager is None: self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager() @@ -881,8 +882,6 @@ class CuraApplication(QtApplication): activityChanged = pyqtSignal() sceneBoundingBoxChanged = pyqtSignal() preferredOutputMimetypeChanged = pyqtSignal() - numBuildPlatesChanged = pyqtSignal() - activeBuildPlateChanged = pyqtSignal() @pyqtProperty(bool, notify = activityChanged) def platformActivity(self): @@ -1130,12 +1129,13 @@ class CuraApplication(QtApplication): nodes.append(node) job = ArrangeObjectsAllBuildPlatesJob(nodes) job.start() - self.setActiveBuildPlate(0) + self.getBuildPlateModel().setActiveBuildPlate(0) # Single build plate @pyqtSlot() def arrangeAll(self): nodes = [] + active_build_plate = self.getBuildPlateModel().activeBuildPlate for node in DepthFirstIterator(self.getController().getScene().getRoot()): if not issubclass(type(node), SceneNode): continue @@ -1147,7 +1147,7 @@ class CuraApplication(QtApplication): continue # i.e. node with layer data if not node.callDecoration("isSliceable"): continue # i.e. node with layer data - if node.callDecoration("getBuildPlateNumber") == self._active_build_plate: + if node.callDecoration("getBuildPlateNumber") == 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) @@ -1284,7 +1284,7 @@ class CuraApplication(QtApplication): group_decorator = GroupDecorator() group_node.addDecorator(group_decorator) group_node.addDecorator(ConvexHullDecorator()) - group_node.addDecorator(BuildPlateDecorator(self.activeBuildPlate)) + group_node.addDecorator(BuildPlateDecorator(self.getBuildPlateModel().activeBuildPlate)) group_node.setParent(self.getController().getScene().getRoot()) group_node.setSelectable(True) center = Selection.getSelectionCenter() @@ -1510,43 +1510,3 @@ class CuraApplication(QtApplication): node = node.getParent() Selection.add(node) - - #### research - hacky place for these kind of thing - @pyqtSlot(int) - def setActiveBuildPlate(self, nr): - if nr == self._active_build_plate: - return - Logger.log("d", "Select build plate: %s" % nr) - self._active_build_plate = nr - - self.activeBuildPlateChanged.emit() - - @pyqtSlot() - def newBuildPlate(self): - Logger.log("d", "New build plate") - #self._num_build_plates += 1 - self.numBuildPlatesChanged.emit() - - @pyqtProperty(int, notify = numBuildPlatesChanged) - def numBuildPlates(self): - return self._num_build_plates - - @pyqtProperty(int, notify = activeBuildPlateChanged) - def activeBuildPlate(self): - return self._active_build_plate - - def updateMaxBuildPlate(self, source): - if not issubclass(type(source), SceneNode): - return - num_build_plates = self._calcMaxBuildPlate() - if num_build_plates != self._num_build_plates: - self._num_build_plates = num_build_plates - self.numBuildPlatesChanged.emit() - - def _calcMaxBuildPlate(self): - max_build_plate = 0 - for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if node.callDecoration("isSliceable"): - build_plate_number = node.callDecoration("getBuildPlateNumber") - max_build_plate = max(build_plate_number, max_build_plate) - return max_build_plate diff --git a/cura/ObjectManager.py b/cura/ObjectManager.py index a148c05f28..8361e43d71 100644 --- a/cura/ObjectManager.py +++ b/cura/ObjectManager.py @@ -15,14 +15,15 @@ class ObjectManager(ListModel): def __init__(self): super().__init__() self._last_selected_index = 0 - Application.getInstance().getController().getScene().sceneChanged.connect(self._update_scene_changed) + self._build_plate_model = Application.getInstance().getBuildPlateModel() + Application.getInstance().getController().getScene().sceneChanged.connect(self._update) Preferences.getInstance().preferenceChanged.connect(self._update) - Application.getInstance().activeBuildPlateChanged.connect(self._update) + self._build_plate_model.activeBuildPlateChanged.connect(self._update) def _update(self, *args): nodes = [] filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate") - active_build_plate_number = Application.getInstance().activeBuildPlate + active_build_plate_number = self._build_plate_model.activeBuildPlate for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")): continue @@ -43,12 +44,6 @@ class ObjectManager(ListModel): self.itemsChanged.emit() - def _update_scene_changed(self, *args): - # if args and type(args[0]) is not CuraSceneNode: - # Logger.log("d", " ascdf %s", args) - # return - self._update(*args) - ## Either select or deselect an item @pyqtSlot(int) def changeSelection(self, index): @@ -77,7 +72,7 @@ class ObjectManager(ListModel): Selection.add(node) build_plate_number = node.callDecoration("getBuildPlateNumber") if build_plate_number is not None and build_plate_number != -1: - Application.getInstance().setActiveBuildPlate(build_plate_number) + self._build_plate_model.setActiveBuildPlate(build_plate_number) self._last_selected_index = index diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index ccec76b53d..e68405daf6 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -18,10 +18,10 @@ class CuraSceneNode(SceneNode): return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0 def isVisible(self): - return super().isVisible() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate + return super().isVisible() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate def isSelectable(self) -> bool: - return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().activeBuildPlate + return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate ## Taken from SceneNode, but replaced SceneNode with CuraSceneNode def __deepcopy__(self, memo): diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index f8b68e118a..c250b9019c 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -70,7 +70,7 @@ class CuraEngineBackend(QObject, Backend): # Workaround to disable layer view processing if layer view is not active. self._layer_view_active = False Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged) - Application.getInstance().activeBuildPlateChanged.connect(self._onActiveViewChanged) + Application.getInstance().getBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveViewChanged) self._onActiveViewChanged() self._stored_layer_data = [] self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob @@ -564,7 +564,7 @@ class CuraEngineBackend(QObject, Backend): Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time ) # See if we need to process the sliced layers job. - active_build_plate = Application.getInstance().activeBuildPlate + active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate: self._startProcessSlicedLayersJob(active_build_plate) # self._onActiveViewChanged() @@ -682,7 +682,7 @@ class CuraEngineBackend(QObject, Backend): application = Application.getInstance() view = application.getController().getActiveView() if view: - active_build_plate = application.activeBuildPlate + active_build_plate = application.getBuildPlateModel().activeBuildPlate if view.getPluginId() == "LayerView": # If switching to layer view, we should process the layers if that hasn't been done yet. self._layer_view_active = True # There is data and we're not slicing at the moment diff --git a/plugins/LayerView/LayerPass.py b/plugins/LayerView/LayerPass.py index 2b0e82d4b3..e6f60df723 100755 --- a/plugins/LayerView/LayerPass.py +++ b/plugins/LayerView/LayerPass.py @@ -66,7 +66,7 @@ class LayerPass(RenderPass): self.bind() tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay) - active_build_plate = Application.getInstance().activeBuildPlate + active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate for node in DepthFirstIterator(self._scene.getRoot()): diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index 699cec23f5..973b513794 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -71,11 +71,9 @@ class SolidView(View): else: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) - activeBuildPlateNumber = Application.getInstance().activeBuildPlate - for node in DepthFirstIterator(scene.getRoot()): if not node.render(renderer): - if node.getMeshData() and node.isVisible(): # and (node.callDecoration("getBuildPlateNumber") == activeBuildPlateNumber): + if node.getMeshData() and node.isVisible(): uniforms = {} shade_factor = 1.0 diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 29d8b439a8..1db6e2a511 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -324,6 +324,7 @@ UM.MainWindow } } + /* Button { id: openFileButton; @@ -339,17 +340,19 @@ UM.MainWindow } action: Cura.Actions.open; } + */ Button { id: objectsButton; text: catalog.i18nc("@action:button","Objects"); - iconSource: UM.Theme.getIcon("load") + iconSource: UM.Theme.getIcon("plus") style: UM.Theme.styles.tool_button tooltip: ''; anchors { - top: openFileButton.bottom; + top: topbar.bottom; + //top: openFileButton.bottom; topMargin: UM.Theme.getSize("default_margin").height; left: parent.left; } diff --git a/resources/qml/Menus/ContextMenu.qml b/resources/qml/Menus/ContextMenu.qml index 175410773e..2aa3bd6bdb 100644 --- a/resources/qml/Menus/ContextMenu.qml +++ b/resources/qml/Menus/ContextMenu.qml @@ -7,7 +7,7 @@ import QtQuick.Dialogs 1.2 import QtQuick.Window 2.1 import UM 1.2 as UM -import Cura 1.0 as Cura +import Cura 1.2 as Cura Menu { @@ -40,21 +40,21 @@ Menu } MenuSeparator {} - MenuItem { - text: "build plate 0"; - onTriggered: CuraActions.setBuildPlateForSelection(0); - checkable: true - checked: false + Instantiator + { + model: Cura.BuildPlateModel + MenuItem { + text: Cura.BuildPlateModel.getItem(index).name; + onTriggered: CuraActions.setBuildPlateForSelection(Cura.BuildPlateModel.getItem(index).buildPlateNumber); + checkable: true + checked: Cura.BuildPlateModel.getItem(index).buildPlateNumber == Cura.BuildPlateModel.activeBuildPlate + } + onObjectAdded: base.insertItem(index, object); + onObjectRemoved: base.removeItem(object) } MenuItem { - text: "build plate 1"; - onTriggered: CuraActions.setBuildPlateForSelection(1); - checkable: true - checked: false - } - MenuItem { - text: "build plate 2"; - onTriggered: CuraActions.setBuildPlateForSelection(2); + text: "New build plate"; + onTriggered: CuraActions.setBuildPlateForSelection(Cura.BuildPlateModel.maxBuildPlate + 1); checkable: true checked: false } diff --git a/resources/qml/Menus/ViewMenu.qml b/resources/qml/Menus/ViewMenu.qml index 3c5485da32..a78e465c85 100644 --- a/resources/qml/Menus/ViewMenu.qml +++ b/resources/qml/Menus/ViewMenu.qml @@ -5,12 +5,12 @@ import QtQuick 2.2 import QtQuick.Controls 1.1 import UM 1.2 as UM -import Cura 1.0 as Cura +import Cura 1.2 as Cura Menu { title: catalog.i18nc("@title:menu menubar:toplevel", "&View"); - id: menu + id: base enabled: !PrintInformation.preSliced Instantiator { @@ -23,30 +23,27 @@ Menu exclusiveGroup: group; onTriggered: UM.Controller.setActiveView(model.id); } - onObjectAdded: menu.insertItem(index, object) - onObjectRemoved: menu.removeItem(object) + onObjectAdded: base.insertItem(index, object) + onObjectRemoved: base.removeItem(object) } ExclusiveGroup { id: group; } MenuSeparator {} - MenuItem { - text: "build plate 0"; - onTriggered: CuraApplication.setActiveBuildPlate(0); - } - MenuItem { - text: "build plate 1"; - onTriggered: CuraApplication.setActiveBuildPlate(1); - } - MenuItem { - text: "build plate 2"; - onTriggered: CuraApplication.setActiveBuildPlate(2); + MenuItem { action: Cura.Actions.homeCamera; } + + MenuSeparator {} + Instantiator + { + model: Cura.BuildPlateModel + MenuItem { + text: Cura.BuildPlateModel.getItem(index).name; + onTriggered: Cura.BuildPlateModel.setActiveBuildPlate(Cura.BuildPlateModel.getItem(index).buildPlateNumber); + checkable: true; + checked: Cura.BuildPlateModel.getItem(index).buildPlateNumber == Cura.BuildPlateModel.activeBuildPlate; + exclusiveGroup: buildPlateGroup; + } + onObjectAdded: base.insertItem(index, object); + onObjectRemoved: base.removeItem(object) } ExclusiveGroup { id: buildPlateGroup; } - - MenuItem { - text: "New build plate"; - onTriggered: CuraApplication.newBuildPlate(); - } - MenuSeparator {} - MenuItem { action: Cura.Actions.homeCamera; } } diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index 4eeb0cd568..bf7d92c4d6 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -52,7 +52,6 @@ Rectangle id: nodeNameLabel anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("default_margin").width - //anchors.right: parent.right width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30 text: Cura.ObjectManager.getItem(index) ? Cura.ObjectManager.getItem(index).name : ""; color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : (Cura.ObjectManager.getItem(index).isOutsideBuildArea ? palette.mid : palette.text) @@ -66,7 +65,7 @@ Rectangle anchors.left: nodeNameLabel.right anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.right: parent.right - text: Cura.ObjectManager.getItem(index) ? Cura.ObjectManager.getItem(index).buildPlateNumber : 0; + text: Cura.ObjectManager.getItem(index).buildPlateNumber != -1 ? Cura.ObjectManager.getItem(index).buildPlateNumber + 1 : ""; color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : palette.text elide: Text.ElideRight } @@ -134,41 +133,22 @@ Rectangle } } - ListModel - { - id: buildPlatesModel - - ListElement - { - name: "build plate 0" - buildPlateNumber: 0 - } - ListElement - { - name: "build plate 1" - buildPlateNumber: 1 - } - ListElement - { - name: "build plate 2" - buildPlateNumber: 2 - } - } Component { id: buildPlateDelegate Rectangle { height: childrenRect.height - color: CuraApplication.activeBuildPlate == buildPlateNumber ? palette.highlight : index % 2 ? palette.base : palette.alternateBase + color: Cura.BuildPlateModel.getItem(index).buildPlateNumber == Cura.BuildPlateModel.activeBuildPlate ? palette.highlight : index % 2 ? palette.base : palette.alternateBase width: parent.width Label { + id: buildPlateNameLabel anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("default_margin").width - anchors.right: parent.right - text: name //Cura.ObjectManager.getItem(index).name; - color: CuraApplication.activeBuildPlate == buildPlateNumber ? palette.highlightedText : palette.text + width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30 + text: Cura.BuildPlateModel.getItem(index) ? Cura.BuildPlateModel.getItem(index).name : ""; + color: Cura.BuildPlateModel.activeBuildPlate == index ? palette.highlightedText : palette.text elide: Text.ElideRight } @@ -177,7 +157,7 @@ Rectangle anchors.fill: parent; onClicked: { - CuraApplication.setActiveBuildPlate(buildPlateNumber); + Cura.BuildPlateModel.setActiveBuildPlate(index); } } } @@ -192,7 +172,6 @@ Rectangle anchors { - // top: objectsList.bottom; topMargin: UM.Theme.getSize("default_margin").height; left: parent.left; leftMargin: UM.Theme.getSize("default_margin").height; @@ -210,21 +189,8 @@ Rectangle ListView { id: buildPlateListView - model: buildPlatesModel - - onModelChanged: - { - //currentIndex = -1; - } + model: Cura.BuildPlateModel 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: buildPlateDelegate } } From f6c7ffac116c0541aff3d8d4d68e66055bc4c8ad Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 14 Nov 2017 14:48:51 +0100 Subject: [PATCH 11/42] CURA-4525 some cleanup and comments --- cura/CuraApplication.py | 1 - cura/GCodeListDecorator.py | 2 +- cura/ObjectManager.py | 1 + cura/Scene/BuildPlateDecorator.py | 1 + plugins/CuraEngineBackend/CuraEngineBackend.py | 15 +++------------ .../CuraEngineBackend/ProcessSlicedLayersJob.py | 11 ----------- 6 files changed, 6 insertions(+), 25 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 08b81d568c..4b71047dfc 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -815,7 +815,6 @@ class CuraApplication(QtApplication): qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel") qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator") qmlRegisterType(UserChangesModel, "Cura", 1, 1, "UserChangesModel") - # qmlRegisterSingletonType(ObjectManager, "Cura", 1, 0, "ObjectManager", ObjectManager.createObjectManager) qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager) diff --git a/cura/GCodeListDecorator.py b/cura/GCodeListDecorator.py index 66ecf3beac..5738d0a7f2 100644 --- a/cura/GCodeListDecorator.py +++ b/cura/GCodeListDecorator.py @@ -4,7 +4,7 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator class GCodeListDecorator(SceneNodeDecorator): def __init__(self): super().__init__() - self._gcode_list = {} # [] + self._gcode_list = [] def getGCodeList(self): return self._gcode_list diff --git a/cura/ObjectManager.py b/cura/ObjectManager.py index 8361e43d71..413f43ed73 100644 --- a/cura/ObjectManager.py +++ b/cura/ObjectManager.py @@ -11,6 +11,7 @@ from PyQt5.QtWidgets import QApplication from UM.Preferences import Preferences +## Keep track of all objects in the project class ObjectManager(ListModel): def __init__(self): super().__init__() diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index 6b4dcfc970..cfbe792699 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -3,6 +3,7 @@ from UM.Application import Application from UM.Logger import Logger +## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator. class BuildPlateDecorator(SceneNodeDecorator): def __init__(self, build_plate_number = -1): super().__init__() diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index c250b9019c..b0f44e931d 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -112,7 +112,6 @@ class CuraEngineBackend(QObject, Backend): self._tool_active = False # If a tool is active, some tasks do not have to do anything self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness. self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers. - # self._need_slicing = False self._build_plates_to_be_sliced = [] # what needs slicing? self._engine_is_fresh = True # Is the newly started engine used before or not? @@ -446,11 +445,6 @@ class CuraEngineBackend(QObject, Backend): self._invokeSlice() - # #self.needsSlicing() - # self.stopSlicing() - # #self._onChanged() - # self._invokeSlice() - ## Called when an error occurs in the socket connection towards the engine. # # \param error The exception that occurred. @@ -476,12 +470,9 @@ class CuraEngineBackend(QObject, Backend): node.getParent().removeChild(node) def markSliceAll(self): - if 0 not in self._build_plates_to_be_sliced: - self._build_plates_to_be_sliced.append(0) - if 1 not in self._build_plates_to_be_sliced: - self._build_plates_to_be_sliced.append(1) - if 2 not in self._build_plates_to_be_sliced: - self._build_plates_to_be_sliced.append(2) + for build_plate_number in range(Application.getInstance().getBuildPlateModel().maxBuildPlate + 1): + if build_plate_number not in self._build_plates_to_be_sliced: + self._build_plates_to_be_sliced.append(build_plate_number) ## Convenient function: mark everything to slice, emit state and clear layer data def needsSlicing(self): diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 916cc4d914..8da18a066d 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -84,17 +84,6 @@ class ProcessSlicedLayersJob(Job): new_node = SceneNode() new_node.addDecorator(BuildPlateDecorator(self._build_plate_number)) - # ## Remove old layer data (if any) - # for node in DepthFirstIterator(self._scene.getRoot()): - # if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number: - # Logger.log("d", " # Removing: %s", node) - # node.getParent().removeChild(node) - # #break - # if self._abort_requested: - # if self._progress_message: - # self._progress_message.hide() - # return - # Force garbage collection. # For some reason, Python has a tendency to keep the layer data # in memory longer than needed. Forcing the GC to run here makes From 4a893c048e28259819aac59f72e0e54d628e76f7 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 14 Nov 2017 16:35:37 +0100 Subject: [PATCH 12/42] CURA-4525 made PrintInformation multi buildplate-aware --- cura/BuildPlateModel.py | 2 +- cura/CuraApplication.py | 2 + cura/PrintInformation.py | 106 ++++++++++++------ .../CuraEngineBackend/CuraEngineBackend.py | 9 +- 4 files changed, 81 insertions(+), 38 deletions(-) diff --git a/cura/BuildPlateModel.py b/cura/BuildPlateModel.py index 35a311dc5f..a29dd65de4 100644 --- a/cura/BuildPlateModel.py +++ b/cura/BuildPlateModel.py @@ -15,7 +15,7 @@ class BuildPlateModel(ListModel): Application.getInstance().getController().getScene().sceneChanged.connect(self.updateMaxBuildPlate) # it may be a bit inefficient when changing a lot simultaneously self._max_build_plate = 1 # default - self._active_build_plate = 0 + self._active_build_plate = -1 @pyqtSlot(int) def setActiveBuildPlate(self, nr): diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 4b71047dfc..46d4270da7 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -758,6 +758,8 @@ class CuraApplication(QtApplication): def getBuildPlateModel(self, *args): if self._build_plate_model is None: self._build_plate_model = BuildPlateModel.createBuildPlateModel() + self._build_plate_model.setActiveBuildPlate(0) # default value + return self._build_plate_model def getSettingInheritanceManager(self, *args): diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index d3bcc10781..6bf35f49c7 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -53,10 +53,10 @@ class PrintInformation(QObject): self.initializeCuraMessagePrintTimeProperties() - self._material_lengths = [] - self._material_weights = [] - self._material_costs = [] - self._material_names = [] + self._material_lengths = {} # indexed by build plate number + self._material_weights = {} + self._material_costs = {} + self._material_names = {} self._pre_sliced = False @@ -68,10 +68,13 @@ class PrintInformation(QObject): self._abbr_machine = "" self._job_name = "" self._project_name = "" + self._active_build_plate = 0 + self._initVariablesWithBuildPlate(self._active_build_plate) Application.getInstance().globalContainerStackChanged.connect(self._updateJobName) Application.getInstance().fileLoaded.connect(self.setBaseName) Application.getInstance().projectFileLoaded.connect(self.setProjectName) + Application.getInstance().getBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged) Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) self._active_material_container = None @@ -83,7 +86,7 @@ class PrintInformation(QObject): # Crate cura message translations and using translation keys initialize empty time Duration object for total time # and time for each feature def initializeCuraMessagePrintTimeProperties(self): - self._current_print_time = Duration(None, self) + self._current_print_time = {} # Duration(None, self) self._print_time_message_translations = { "inset_0": catalog.i18nc("@tooltip", "Outer Wall"), @@ -101,10 +104,26 @@ class PrintInformation(QObject): self._print_time_message_values = {} - # Full fill message values using keys from _print_time_message_translations - for key in self._print_time_message_translations.keys(): - self._print_time_message_values[key] = Duration(None, self) + def _initPrintTimeMessageValues(self, build_plate_number): + # Full fill message values using keys from _print_time_message_translations + self._print_time_message_values[build_plate_number] = {} + for key in self._print_time_message_translations.keys(): + self._print_time_message_values[build_plate_number][key] = Duration(None, self) + + def _initVariablesWithBuildPlate(self, build_plate_number): + if build_plate_number not in self._print_time_message_values: + self._initPrintTimeMessageValues(build_plate_number) + if self._active_build_plate not in self._material_lengths: + self._material_lengths[self._active_build_plate] = [] + if self._active_build_plate not in self._material_weights: + self._material_weights[self._active_build_plate] = [] + if self._active_build_plate not in self._material_costs: + self._material_costs[self._active_build_plate] = [] + if self._active_build_plate not in self._material_names: + self._material_names[self._active_build_plate] = [] + if self._active_build_plate not in self._current_print_time: + self._current_print_time[self._active_build_plate] = Duration(None, self) currentPrintTimeChanged = pyqtSignal() @@ -120,53 +139,58 @@ class PrintInformation(QObject): @pyqtProperty(Duration, notify = currentPrintTimeChanged) def currentPrintTime(self): - return self._current_print_time + return self._current_print_time[self._active_build_plate] materialLengthsChanged = pyqtSignal() @pyqtProperty("QVariantList", notify = materialLengthsChanged) def materialLengths(self): - return self._material_lengths + return self._material_lengths[self._active_build_plate] materialWeightsChanged = pyqtSignal() @pyqtProperty("QVariantList", notify = materialWeightsChanged) def materialWeights(self): - return self._material_weights + return self._material_weights[self._active_build_plate] materialCostsChanged = pyqtSignal() @pyqtProperty("QVariantList", notify = materialCostsChanged) def materialCosts(self): - return self._material_costs + return self._material_costs[self._active_build_plate] materialNamesChanged = pyqtSignal() @pyqtProperty("QVariantList", notify = materialNamesChanged) def materialNames(self): - return self._material_names + return self._material_names[self._active_build_plate] - def _onPrintDurationMessage(self, print_time, material_amounts): - - self._updateTotalPrintTimePerFeature(print_time) + def _onPrintDurationMessage(self, build_plate_number, print_time, material_amounts): + Logger.log("d", " ### print duration message for build plate %s", build_plate_number) + self._updateTotalPrintTimePerFeature(build_plate_number, print_time) self.currentPrintTimeChanged.emit() self._material_amounts = material_amounts self._calculateInformation() - def _updateTotalPrintTimePerFeature(self, print_time): + def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time): total_estimated_time = 0 + if build_plate_number not in self._print_time_message_values: + self._initPrintTimeMessageValues(build_plate_number) + for feature, time in print_time.items(): if time != time: # Check for NaN. Engine can sometimes give us weird values. - self._print_time_message_values.get(feature).setDuration(0) + self._print_time_message_values[build_plate_number].get(feature).setDuration(0) Logger.log("w", "Received NaN for print duration message") continue total_estimated_time += time - self._print_time_message_values.get(feature).setDuration(time) + self._print_time_message_values[build_plate_number].get(feature).setDuration(time) - self._current_print_time.setDuration(total_estimated_time) + if build_plate_number not in self._current_print_time: + self._current_print_time[build_plate_number] = Duration(None, self) + self._current_print_time[build_plate_number].setDuration(total_estimated_time) def _calculateInformation(self): if Application.getInstance().getGlobalContainerStack() is None: @@ -174,10 +198,10 @@ class PrintInformation(QObject): # Material amount is sent as an amount of mm^3, so calculate length from that radius = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2 - self._material_lengths = [] - self._material_weights = [] - self._material_costs = [] - self._material_names = [] + self._material_lengths[self._active_build_plate] = [] + self._material_weights[self._active_build_plate] = [] + self._material_costs[self._active_build_plate] = [] + self._material_names[self._active_build_plate] = [] material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings")) @@ -215,10 +239,10 @@ class PrintInformation(QObject): length = round((amount / (math.pi * radius ** 2)) / 1000, 2) else: length = 0 - self._material_weights.append(weight) - self._material_lengths.append(length) - self._material_costs.append(cost) - self._material_names.append(material_name) + self._material_weights[self._active_build_plate].append(weight) + self._material_lengths[self._active_build_plate].append(length) + self._material_costs[self._active_build_plate].append(cost) + self._material_names[self._active_build_plate].append(material_name) self.materialLengthsChanged.emit() self.materialWeightsChanged.emit() @@ -245,6 +269,20 @@ class PrintInformation(QObject): self._active_material_container = active_material_containers[0] self._active_material_container.metaDataChanged.connect(self._onMaterialMetaDataChanged) + def _onActiveBuildPlateChanged(self): + new_active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate + if new_active_build_plate != self._active_build_plate: + Logger.log("d", " ## active build plate changed: %s", self._active_build_plate) + self._active_build_plate = new_active_build_plate + + self._initVariablesWithBuildPlate(self._active_build_plate) + + self.materialLengthsChanged.emit() + self.materialWeightsChanged.emit() + self.materialCostsChanged.emit() + self.materialNamesChanged.emit() + self.currentPrintTimeChanged.emit() + def _onMaterialMetaDataChanged(self, *args, **kwargs): self._calculateInformation() @@ -341,7 +379,9 @@ class PrintInformation(QObject): @pyqtSlot(result = "QVariantMap") def getFeaturePrintTimes(self): result = {} - for feature, time in self._print_time_message_values.items(): + if self._active_build_plate not in self._print_time_message_values: + self._initPrintTimeMessageValues(self._active_build_plate) + for feature, time in self._print_time_message_values[self._active_build_plate].items(): if feature in self._print_time_message_translations: result[self._print_time_message_translations[feature]] = time else: @@ -349,10 +389,12 @@ class PrintInformation(QObject): return result # Simulate message with zero time duration - def setToZeroPrintInformation(self): + def setToZeroPrintInformation(self, build_plate_number): temp_message = {} - for key in self._print_time_message_values.keys(): + if build_plate_number not in self._print_time_message_values: + self._print_time_message_values[build_plate_number] = {} + for key in self._print_time_message_values[build_plate_number].keys(): temp_message[key] = 0 temp_material_amounts = [0] - self._onPrintDurationMessage(temp_message, temp_material_amounts) + self._onPrintDurationMessage(build_plate_number, temp_message, temp_material_amounts) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index b0f44e931d..859578c3e9 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -207,6 +207,7 @@ class CuraEngineBackend(QObject, Backend): return # see if we really have to slice + active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0) Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced) num_objects = self._numObjects() @@ -218,8 +219,8 @@ class CuraEngineBackend(QObject, Backend): self._stored_layer_data = [] self._stored_optimized_layer_data[build_plate_to_be_sliced] = [] - if Application.getInstance().getPrintInformation(): - Application.getInstance().getPrintInformation().setToZeroPrintInformation() + if Application.getInstance().getPrintInformation() and build_plate_to_be_sliced == active_build_plate: + Application.getInstance().getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced) if self._process is None: self._createSocket() @@ -547,11 +548,9 @@ class CuraEngineBackend(QObject, Backend): replaced = replaced.replace("{filament_cost}", str(Application.getInstance().getPrintInformation().materialCosts)) replaced = replaced.replace("{jobname}", str(Application.getInstance().getPrintInformation().jobName)) - #gcode_list[gcode_list.index(line)] = replaced gcode_list[index] = replaced self._slicing = False - #self._need_slicing = False Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time ) # See if we need to process the sliced layers job. @@ -608,7 +607,7 @@ class CuraEngineBackend(QObject, Backend): material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount) times = self._parseMessagePrintTimes(message) - self.printDurationMessage.emit(times, material_amounts) + self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts) ## Called for parsing message to retrieve estimated time per feature # From be6561b5754d8b13bf51b5cb990907724787797f Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 15 Nov 2017 10:28:34 +0100 Subject: [PATCH 13/42] CURA-4525 fixed material info per build plate, bugfix arrange on load --- cura/CuraApplication.py | 2 +- cura/PrintInformation.py | 29 +++++++++++-------- .../CuraEngineBackend/CuraEngineBackend.py | 1 - plugins/GCodeWriter/GCodeWriter.py | 3 +- plugins/SliceInfoPlugin/SliceInfo.py | 2 +- 5 files changed, 21 insertions(+), 16 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 46d4270da7..14b09c4902 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1428,7 +1428,7 @@ class CuraApplication(QtApplication): self.fileLoaded.emit(filename) arrange_objects_on_load = Preferences.getInstance().getValue("cura/arrange_objects_on_load") - target_build_plate = self.activeBuildPlate if arrange_objects_on_load else -1 + target_build_plate = self.getBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1 for original_node in nodes: node = CuraSceneNode() # We want our own CuraSceneNode diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 6bf35f49c7..bbc5cd4329 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -165,13 +165,16 @@ class PrintInformation(QObject): def materialNames(self): return self._material_names[self._active_build_plate] + def printTimes(self): + return self._print_time_message_values[self._active_build_plate] + def _onPrintDurationMessage(self, build_plate_number, print_time, material_amounts): Logger.log("d", " ### print duration message for build plate %s", build_plate_number) self._updateTotalPrintTimePerFeature(build_plate_number, print_time) self.currentPrintTimeChanged.emit() self._material_amounts = material_amounts - self._calculateInformation() + self._calculateInformation(build_plate_number) def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time): total_estimated_time = 0 @@ -192,16 +195,16 @@ class PrintInformation(QObject): self._current_print_time[build_plate_number] = Duration(None, self) self._current_print_time[build_plate_number].setDuration(total_estimated_time) - def _calculateInformation(self): + def _calculateInformation(self, build_plate_number): if Application.getInstance().getGlobalContainerStack() is None: return # Material amount is sent as an amount of mm^3, so calculate length from that radius = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2 - self._material_lengths[self._active_build_plate] = [] - self._material_weights[self._active_build_plate] = [] - self._material_costs[self._active_build_plate] = [] - self._material_names[self._active_build_plate] = [] + self._material_lengths[build_plate_number] = [] + self._material_weights[build_plate_number] = [] + self._material_costs[build_plate_number] = [] + self._material_names[build_plate_number] = [] material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings")) @@ -239,10 +242,10 @@ class PrintInformation(QObject): length = round((amount / (math.pi * radius ** 2)) / 1000, 2) else: length = 0 - self._material_weights[self._active_build_plate].append(weight) - self._material_lengths[self._active_build_plate].append(length) - self._material_costs[self._active_build_plate].append(cost) - self._material_names[self._active_build_plate].append(material_name) + self._material_weights[build_plate_number].append(weight) + self._material_lengths[build_plate_number].append(length) + self._material_costs[build_plate_number].append(cost) + self._material_names[build_plate_number].append(material_name) self.materialLengthsChanged.emit() self.materialWeightsChanged.emit() @@ -253,7 +256,8 @@ class PrintInformation(QObject): if preference != "cura/material_settings": return - self._calculateInformation() + for build_plate_number in range(Application.getInstance().getBuildPlateModel().maxBuildPlate + 1): + self._calculateInformation(build_plate_number) def _onActiveMaterialChanged(self): if self._active_material_container: @@ -284,7 +288,8 @@ class PrintInformation(QObject): self.currentPrintTimeChanged.emit() def _onMaterialMetaDataChanged(self, *args, **kwargs): - self._calculateInformation() + for build_plate_number in range(Application.getInstance().getBuildPlateModel().maxBuildPlate + 1): + self._calculateInformation(build_plate_number) @pyqtSlot(str) def setJobName(self, name): diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 859578c3e9..08c89c56a6 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -433,7 +433,6 @@ class CuraEngineBackend(QObject, Backend): self._postponed_scene_change_sources.append(source) return - Logger.log("d", " going to reslice: %s", build_plate_changed) self.stopSlicing() for build_plate_number in build_plate_changed: if build_plate_number not in self._build_plates_to_be_sliced: diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index 3860590ef7..192354d947 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -59,8 +59,9 @@ class GCodeWriter(MeshWriter): Logger.log("e", "GCode Writer does not support non-text mode.") return False + active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate scene = Application.getInstance().getController().getScene() - gcode_list = getattr(scene, "gcode_list") + gcode_list = getattr(scene, "gcode_list")[active_build_plate] if gcode_list: for gcode in gcode_list: stream.write(gcode) diff --git a/plugins/SliceInfoPlugin/SliceInfo.py b/plugins/SliceInfoPlugin/SliceInfo.py index 0514c4dacf..a72c056bc9 100755 --- a/plugins/SliceInfoPlugin/SliceInfo.py +++ b/plugins/SliceInfoPlugin/SliceInfo.py @@ -162,7 +162,7 @@ class SliceInfo(Extension): data["models"].append(model) - print_times = print_information._print_time_message_values + print_times = print_information.printTimes() data["print_times"] = {"travel": int(print_times["travel"].getDisplayString(DurationFormat.Format.Seconds)), "support": int(print_times["support"].getDisplayString(DurationFormat.Format.Seconds)), "infill": int(print_times["infill"].getDisplayString(DurationFormat.Format.Seconds)), From 864f41772355e6eef4d0170dd9090b870b8d368a Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 15 Nov 2017 16:03:32 +0100 Subject: [PATCH 14/42] CURA-4525 Fix load and save projects. Not storing build plates or object names yet. --- cura/PrintInformation.py | 2 -- plugins/3MFReader/ThreeMFReader.py | 13 ++++++++++--- plugins/3MFWriter/ThreeMFWriter.py | 3 ++- resources/qml/Actions.qml | 4 ++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index bbc5cd4329..f2d28c9297 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -169,7 +169,6 @@ class PrintInformation(QObject): return self._print_time_message_values[self._active_build_plate] def _onPrintDurationMessage(self, build_plate_number, print_time, material_amounts): - Logger.log("d", " ### print duration message for build plate %s", build_plate_number) self._updateTotalPrintTimePerFeature(build_plate_number, print_time) self.currentPrintTimeChanged.emit() @@ -276,7 +275,6 @@ class PrintInformation(QObject): def _onActiveBuildPlateChanged(self): new_active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate if new_active_build_plate != self._active_build_plate: - Logger.log("d", " ## active build plate changed: %s", self._active_build_plate) self._active_build_plate = new_active_build_plate self._initVariablesWithBuildPlate(self._active_build_plate) diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 8c4ef9d1ae..f75cf68312 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -15,8 +15,8 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator from UM.Application import Application from cura.Settings.ExtruderManager import ExtruderManager from cura.QualityManager import QualityManager -#from UM.Scene.SceneNode import SceneNode -from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode +from cura.Scene.CuraSceneNode import CuraSceneNode +from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.ZOffsetDecorator import ZOffsetDecorator @@ -44,6 +44,7 @@ class ThreeMFReader(MeshReader): } self._base_name = "" self._unit = None + self._object_count = 0 # Used to name objects as there is no node name yet. def _createMatrixFromTransformationString(self, transformation): if transformation == "": @@ -78,7 +79,12 @@ class ThreeMFReader(MeshReader): ## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium scene node. # \returns Uranium scene node. def _convertSavitarNodeToUMNode(self, savitar_node): - um_node = SceneNode() + self._object_count += 1 + node_name = "Object %s" % self._object_count + + um_node = CuraSceneNode() + um_node.addDecorator(BuildPlateDecorator(0)) + um_node.setName(node_name) transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation()) um_node.setTransformation(transformation) mesh_builder = MeshBuilder() @@ -155,6 +161,7 @@ class ThreeMFReader(MeshReader): def read(self, file_name): result = [] + self._object_count = 0 # Used to name objects as there is no node name yet. # The base object of 3mf is a zipped archive. try: archive = zipfile.ZipFile(file_name, "r") diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index a764d30fac..8e005c47b1 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -7,6 +7,7 @@ from UM.Logger import Logger from UM.Math.Matrix import Matrix from UM.Application import Application import UM.Scene.SceneNode +from cura.Scene.CuraSceneNode import CuraSceneNode import Savitar @@ -63,7 +64,7 @@ class ThreeMFWriter(MeshWriter): ## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode # \returns Uranium Scenen node. def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()): - if type(um_node) is not UM.Scene.SceneNode.SceneNode: + if type(um_node) not in [UM.Scene.SceneNode.SceneNode, CuraSceneNode]: return None savitar_node = Savitar.SceneNode() diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index 89ec2cf70d..c6b0a443bb 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -304,8 +304,8 @@ Item Action { id: arrangeAllBuildPlatesAction; - text: ""; - iconName: "document-open"; + text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models To All Build Plates"); + //iconName: "document-open"; onTriggered: CuraApplication.arrangeObjectsToAllBuildPlates(); } From e2a663992cf9e8c5f6062114369ba63c7ab18fc9 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 15 Nov 2017 17:00:19 +0100 Subject: [PATCH 15/42] CURA-4525 Added an option to turn on the UI elements of multi build plate --- cura/CuraApplication.py | 5 ++++- resources/qml/Cura.qml | 7 ++++--- resources/qml/Menus/ContextMenu.qml | 8 ++++++-- resources/qml/Menus/ViewMenu.qml | 5 ++++- resources/qml/Preferences/GeneralPage.qml | 18 ++++++++++++++++-- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 14b09c4902..056b6eaeb2 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -311,6 +311,7 @@ class CuraApplication(QtApplication): preferences.addPreference("cura/choice_on_profile_override", "always_ask") preferences.addPreference("cura/choice_on_open_project", "always_ask") preferences.addPreference("cura/arrange_objects_on_load", True) + preferences.addPreference("cura/use_multi_build_plate", False) preferences.addPreference("cura/currency", "€") preferences.addPreference("cura/material_settings", "{}") @@ -1427,7 +1428,9 @@ class CuraApplication(QtApplication): min_offset = 8 self.fileLoaded.emit(filename) - arrange_objects_on_load = Preferences.getInstance().getValue("cura/arrange_objects_on_load") + arrange_objects_on_load = ( + not Preferences.getInstance().getValue("cura/use_multi_build_plate") or + Preferences.getInstance().getValue("cura/arrange_objects_on_load")) target_build_plate = self.getBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1 for original_node in nodes: diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 1db6e2a511..254679ec53 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -324,10 +324,10 @@ UM.MainWindow } } - /* Button { id: openFileButton; + visible: !UM.Preferences.getValue("cura/use_multi_build_plate") text: catalog.i18nc("@action:button","Open File"); iconSource: UM.Theme.getIcon("load") style: UM.Theme.styles.tool_button @@ -340,12 +340,13 @@ UM.MainWindow } action: Cura.Actions.open; } - */ Button { id: objectsButton; - text: catalog.i18nc("@action:button","Objects"); + visible: UM.Preferences.getValue("cura/use_multi_build_plate") + + text: catalog.i18nc("@action:button","Objects list"); iconSource: UM.Theme.getIcon("plus") style: UM.Theme.styles.tool_button tooltip: ''; diff --git a/resources/qml/Menus/ContextMenu.qml b/resources/qml/Menus/ContextMenu.qml index 2aa3bd6bdb..a52a2d46a3 100644 --- a/resources/qml/Menus/ContextMenu.qml +++ b/resources/qml/Menus/ContextMenu.qml @@ -39,7 +39,9 @@ Menu onObjectRemoved: base.removeItem(object) } - MenuSeparator {} + MenuSeparator { + visible: UM.Preferences.getValue("cura/use_multi_build_plate") + } Instantiator { model: Cura.BuildPlateModel @@ -48,15 +50,17 @@ Menu onTriggered: CuraActions.setBuildPlateForSelection(Cura.BuildPlateModel.getItem(index).buildPlateNumber); checkable: true checked: Cura.BuildPlateModel.getItem(index).buildPlateNumber == Cura.BuildPlateModel.activeBuildPlate + visible: UM.Preferences.getValue("cura/use_multi_build_plate") } onObjectAdded: base.insertItem(index, object); - onObjectRemoved: base.removeItem(object) + onObjectRemoved: base.removeItem(object); } MenuItem { text: "New build plate"; onTriggered: CuraActions.setBuildPlateForSelection(Cura.BuildPlateModel.maxBuildPlate + 1); checkable: true checked: false + visible: UM.Preferences.getValue("cura/use_multi_build_plate") } // Global actions diff --git a/resources/qml/Menus/ViewMenu.qml b/resources/qml/Menus/ViewMenu.qml index a78e465c85..7c7060b1f0 100644 --- a/resources/qml/Menus/ViewMenu.qml +++ b/resources/qml/Menus/ViewMenu.qml @@ -31,7 +31,9 @@ Menu MenuSeparator {} MenuItem { action: Cura.Actions.homeCamera; } - MenuSeparator {} + MenuSeparator { + visible: UM.Preferences.getValue("cura/use_multi_build_plate") + } Instantiator { model: Cura.BuildPlateModel @@ -41,6 +43,7 @@ Menu checkable: true; checked: Cura.BuildPlateModel.getItem(index).buildPlateNumber == Cura.BuildPlateModel.activeBuildPlate; exclusiveGroup: buildPlateGroup; + visible: UM.Preferences.getValue("cura/use_multi_build_plate") } onObjectAdded: base.insertItem(index, object); onObjectRemoved: base.removeItem(object) diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index c74fb5720d..197e22fb53 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -454,12 +454,26 @@ UM.PreferencesPage UM.TooltipArea { width: childrenRect.width height: childrenRect.height - text: catalog.i18nc("@info:tooltip","Should newly loaded models be arranged on the build palte?") + text: catalog.i18nc("@info:tooltip","Use multi build plate functionality (EXPERIMENTAL)") + + CheckBox + { + id: useMultiBuildPlateCheckbox + text: catalog.i18nc("@option:check","Use multi build plate functionality (EXPERIMENTAL, restart)") + checked: boolCheck(UM.Preferences.getValue("cura/use_multi_build_plate")) + onCheckedChanged: UM.Preferences.setValue("cura/use_multi_build_plate", checked) + } + } + + UM.TooltipArea { + width: childrenRect.width + height: childrenRect.height + text: catalog.i18nc("@info:tooltip","Should newly loaded models be arranged on the build plate? Used in conjunction with multi build plate (EXPERIMENTAL)") CheckBox { id: arrangeOnLoadCheckbox - text: catalog.i18nc("@option:check","Arrange objects on load") + text: catalog.i18nc("@option:check","Arrange objects on load (EXPERIMENTAL)") checked: boolCheck(UM.Preferences.getValue("cura/arrange_objects_on_load")) onCheckedChanged: UM.Preferences.setValue("cura/arrange_objects_on_load", checked) } From 228039545bb4eab3134a1fd75507e2b010749ec3 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 16 Nov 2017 09:58:53 +0100 Subject: [PATCH 16/42] CURA-4525 send active build plate to legacy UM3 with Print over network --- plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py | 3 ++- resources/qml/Actions.qml | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py index d8dd780ed5..8895d1c22d 100755 --- a/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/NetworkPrinterOutputDevice.py @@ -675,7 +675,8 @@ class NetworkPrinterOutputDevice(PrinterOutputDevice): Application.getInstance().showPrintMonitor.emit(True) self._print_finished = True self.writeStarted.emit(self) - self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list") + active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate + self._gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")[active_build_plate] print_information = Application.getInstance().getPrintInformation() warnings = [] # There might be multiple things wrong. Keep a list of all the stuff we need to warn about. diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index c6b0a443bb..e2db0171b1 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -306,7 +306,7 @@ Item id: arrangeAllBuildPlatesAction; text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models To All Build Plates"); //iconName: "document-open"; - onTriggered: CuraApplication.arrangeObjectsToAllBuildPlates(); + onTriggered: Printer.arrangeObjectsToAllBuildPlates(); } Action From 579f2b5ec6b6e57766bd6a5a3c083045601d421d Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 16 Nov 2017 16:34:12 +0100 Subject: [PATCH 17/42] CURA-4525 prepare for print all or single build plate question --- .../UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py index 14a60a6b22..2f56561295 100644 --- a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py @@ -255,7 +255,8 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte self._request_job = [nodes, file_name, filter_by_machine, file_handler, kwargs] # the build plates to be sent - self._job_list = list(getattr(Application.getInstance().getController().getScene(), "gcode_list").keys()) + gcodes = getattr(Application.getInstance().getController().getScene(), "gcode_list") + self._job_list = list(gcodes.keys()) Logger.log("d", "build plates to be sent to printer: %s", (self._job_list)) if self._stage != OutputStage.ready: @@ -268,7 +269,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte return self._add_build_plate_number = len(self._job_list) > 1 - if len(self._printers) > 1: + if len(self._printers) > 1 or len(gcodes) > 1: self.spawnPrintView() # Ask user how to print it. elif len(self._printers) == 1: # If there is only one printer, don't bother asking. From 312bd137c221194a7cfa97db4117495ff9d3a529 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 21 Dec 2017 11:03:32 +0100 Subject: [PATCH 18/42] Fix merge error, a variable got renamed. CURA-4525 --- cura/PrintInformation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 72613fb7ef..60d3c11a49 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -74,7 +74,6 @@ class PrintInformation(QObject): Application.getInstance().globalContainerStackChanged.connect(self._updateJobName) Application.getInstance().fileLoaded.connect(self.setBaseName) - Application.getInstance().projectFileLoaded.connect(self.setProjectName) Application.getInstance().getBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged) Application.getInstance().workspaceLoaded.connect(self.setProjectName) From a47107448ef1476f94ed9406568d4dae24473670 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 21 Dec 2017 11:39:37 +0100 Subject: [PATCH 19/42] Moved objects menu to lower left and made it collapsible. CURA-4525 --- .../ProcessSlicedLayersJob.py | 2 +- resources/qml/Cura.qml | 40 ++------------ resources/qml/ObjectsList.qml | 55 +++++++++++++------ resources/themes/cura-light/theme.json | 4 +- 4 files changed, 47 insertions(+), 54 deletions(-) diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 077e81f8dc..5f632768ec 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -69,7 +69,7 @@ class ProcessSlicedLayersJob(Job): return self._build_plate_number def run(self): - Logger.log("d", "########## Processing new layer for [%s]..." % self._build_plate_number) + Logger.log("d", "Processing new layer for build plate %s..." % self._build_plate_number) start_time = time() view = Application.getInstance().getController().getActiveView() if view.getPluginId() == "SimulationView": diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 9f3ccb67c5..ec9773679a 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -348,7 +348,6 @@ UM.MainWindow Button { id: openFileButton; - visible: !UM.Preferences.getValue("cura/use_multi_build_plate") text: catalog.i18nc("@action:button","Open File"); iconSource: UM.Theme.getIcon("load") style: UM.Theme.styles.tool_button @@ -362,25 +361,6 @@ UM.MainWindow action: Cura.Actions.open; } - Button - { - id: objectsButton; - visible: UM.Preferences.getValue("cura/use_multi_build_plate") - - text: catalog.i18nc("@action:button","Objects list"); - iconSource: UM.Theme.getIcon("plus") - style: UM.Theme.styles.tool_button - tooltip: ''; - anchors - { - top: topbar.bottom; - //top: openFileButton.bottom; - topMargin: UM.Theme.getSize("default_margin").height; - left: parent.left; - } - action: triggerObjectsList; - } - Toolbar { id: toolbar; @@ -389,32 +369,24 @@ UM.MainWindow property int mouseY: base.mouseY anchors { - top: objectsButton.bottom; + top: openFileButton.bottom; topMargin: UM.Theme.getSize("window_margin").height; left: parent.left; } } - Action - { - id: triggerObjectsList; - text: catalog.i18nc("@action:inmenu menubar:file","&Open File(s)..."); - iconName: "document-open"; - shortcut: StandardKey.Open; - onTriggered: objectsList.visible = !objectsList.visible; - } - ObjectsList { id: objectsList; - visible: false; - //z: -10; + visible: UM.Preferences.getValue("cura/use_multi_build_plate"); anchors { - top: objectsButton.top; - left: objectsButton.right; + bottom: parent.bottom; + left: parent.left; leftMargin: UM.Theme.getSize("default_margin").width; rightMargin: UM.Theme.getSize("default_margin").width; + topMargin: UM.Theme.getSize("default_margin").height; + bottomMargin: UM.Theme.getSize("default_margin").height; } } diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index bf7d92c4d6..e0e7e08820 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -19,25 +19,43 @@ Rectangle color: UM.Theme.getColor("tool_panel_background") width: UM.Theme.getSize("objects_menu_size").width - height: UM.Theme.getSize("objects_menu_size").height + height: { + if (collapsed) { + return UM.Theme.getSize("objects_menu_size_collapsed").height; + } else { + return UM.Theme.getSize("objects_menu_size").height; + } + } + + property bool collapsed: false; SystemPalette { id: palette } - Button - { - id: openFileButton; - text: catalog.i18nc("@action:button","Open File"); - iconSource: UM.Theme.getIcon("load") - style: UM.Theme.styles.tool_button - tooltip: ''; - anchors + Button { + id: collapseButton + anchors.top: parent.top + anchors.topMargin: Math.floor(UM.Theme.getSize("default_margin").height + (UM.Theme.getSize("layerview_row").height - UM.Theme.getSize("default_margin").height) / 2) + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("default_margin").width + + width: UM.Theme.getSize("standard_arrow").width + height: UM.Theme.getSize("standard_arrow").height + + onClicked: collapsed = !collapsed + + style: ButtonStyle { - top: parent.top; - topMargin: UM.Theme.getSize("default_margin").height; - left: parent.left; - leftMargin: UM.Theme.getSize("default_margin").height; + background: UM.RecolorImage + { + width: control.width + height: control.height + sourceSize.width: width + sourceSize.height: width + color: UM.Theme.getColor("setting_control_text") + source: collapsed ? UM.Theme.getIcon("arrow_left") : UM.Theme.getIcon("arrow_bottom") + } + label: Label{ } } - action: Cura.Actions.open; } Component { @@ -86,11 +104,12 @@ Rectangle { id: objectsList frameVisible: true + visible: !collapsed width: parent.width - 2 * UM.Theme.getSize("default_margin").height anchors { - top: openFileButton.bottom; + top: collapseButton.bottom; topMargin: UM.Theme.getSize("default_margin").height; left: parent.left; leftMargin: UM.Theme.getSize("default_margin").height; @@ -118,10 +137,11 @@ Rectangle CheckBox { id: filterBuildPlateCheckbox + visible: !collapsed checked: boolCheck(UM.Preferences.getValue("view/filter_current_build_plate")) onClicked: UM.Preferences.setValue("view/filter_current_build_plate", checked) - text: catalog.i18nc("@option:check","Filter active build plate"); + text: catalog.i18nc("@option:check","See only current build plate"); anchors { @@ -133,7 +153,6 @@ Rectangle } } - Component { id: buildPlateDelegate Rectangle @@ -167,7 +186,7 @@ Rectangle { id: buildPlateSelection frameVisible: true - height: 100 + height: UM.Theme.getSize("build_plate_selection_size").height width: parent.width - 2 * UM.Theme.getSize("default_margin").height anchors diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index e7e1a377f5..714e578d97 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -393,6 +393,8 @@ "jobspecs_line": [2.0, 2.0], - "objects_menu_size": [20, 40] + "objects_menu_size": [17, 40], + "objects_menu_size_collapsed": [15, 15], + "build_plate_selection_size": [15, 5] } } From 1ca6fa0daa240f9bac2cf5a431bbb82d43473af7 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 21 Dec 2017 11:40:59 +0100 Subject: [PATCH 20/42] Different arrow for collapsing. CURA-4525 --- resources/qml/ObjectsList.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index e0e7e08820..95979961c0 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -52,7 +52,7 @@ Rectangle sourceSize.width: width sourceSize.height: width color: UM.Theme.getColor("setting_control_text") - source: collapsed ? UM.Theme.getIcon("arrow_left") : UM.Theme.getIcon("arrow_bottom") + source: collapsed ? UM.Theme.getIcon("arrow_top") : UM.Theme.getIcon("arrow_bottom") } label: Label{ } } From 4b5097f99831fb49d0c4db3201bcd211225684a2 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 21 Dec 2017 12:41:06 +0100 Subject: [PATCH 21/42] Objects menu now collapses animated and it has a border. CURA-4525 --- resources/qml/ObjectsList.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index 95979961c0..b67c0c0bec 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -26,6 +26,10 @@ Rectangle return UM.Theme.getSize("objects_menu_size").height; } } + Behavior on height { NumberAnimation { duration: 100 } } + + border.width: UM.Theme.getSize("default_lining").width + border.color: UM.Theme.getColor("lining") property bool collapsed: false; From c05e6b43fff33148ce75230441edacbd8b4d5895 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 21 Dec 2017 13:11:32 +0100 Subject: [PATCH 22/42] Fixed platform physics. CURA-4525 --- cura/PlatformPhysics.py | 2 +- plugins/CuraEngineBackend/CuraEngineBackend.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 23197dac24..cc2074cc3e 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -60,7 +60,7 @@ class PlatformPhysics: random.shuffle(nodes) for node in nodes: - if node is root or type(node) is not SceneNode or node.getBoundingBox() is None: + if node is root or not issubclass(type(node), SceneNode) or node.getBoundingBox() is None: continue bbox = node.getBoundingBox() diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 9d7cb62a8e..473dbee31a 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -213,7 +213,7 @@ class CuraEngineBackend(QObject, Backend): Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced) num_objects = self._numObjects() if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0: - Logger.log("d", " ## Build plate %s has 0 objects to be sliced, skipping", build_plate_to_be_sliced) + Logger.log("d", "Build plate %s has 0 objects to be sliced, skipping", build_plate_to_be_sliced) self._invokeSlice() return From 9f8eae006c77000f3d17c9cb3b4c5873ac6cec4d Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 21 Dec 2017 13:15:10 +0100 Subject: [PATCH 23/42] Fix push free. CURA-4525 --- cura/PlatformPhysics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index cc2074cc3e..5a57912db6 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -80,7 +80,7 @@ class PlatformPhysics: # Check for collisions between convex hulls for other_node in BreadthFirstIterator(root): # Ignore root, ourselves and anything that is not a normal SceneNode. - if other_node is root or type(other_node) is not SceneNode or other_node is node: + if other_node is root or not issubclass(type(other_node), SceneNode) or other_node is node: continue # Ignore collisions of a group with it's own children From 08391250765dc1a01780421a1db86bf13294b85c Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 21 Dec 2017 13:31:19 +0100 Subject: [PATCH 24/42] Fix platform physics not working across different build plates. CURA-4525 --- cura/PlatformPhysics.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 5a57912db6..43118c5d01 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -57,6 +57,7 @@ class PlatformPhysics: # Only check nodes inside build area. nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea)] + active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate random.shuffle(nodes) for node in nodes: @@ -80,7 +81,7 @@ class PlatformPhysics: # Check for collisions between convex hulls for other_node in BreadthFirstIterator(root): # Ignore root, ourselves and anything that is not a normal SceneNode. - if other_node is root or not issubclass(type(other_node), SceneNode) or other_node is node: + if other_node is root or not issubclass(type(other_node), SceneNode) or other_node is node or other_node.callDecoration("getBuildPlateNumber") != node.callDecoration("getBuildPlateNumber"): continue # Ignore collisions of a group with it's own children From fda4badab154e4dcf74e7ec3eca26c5800f9205d Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 21 Dec 2017 15:08:46 +0100 Subject: [PATCH 25/42] Checked build plates in context menu now actually match the selected item's build plates; changed collapse arrow. CURA-4525 --- cura/BuildPlateModel.py | 16 ++++++++++++++++ resources/qml/Menus/ContextMenu.qml | 7 +++++-- resources/qml/ObjectsList.qml | 2 +- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/cura/BuildPlateModel.py b/cura/BuildPlateModel.py index a29dd65de4..c832a7c522 100644 --- a/cura/BuildPlateModel.py +++ b/cura/BuildPlateModel.py @@ -2,6 +2,7 @@ from UM.Qt.ListModel import ListModel from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode +from UM.Scene.Selection import Selection from UM.Logger import Logger from UM.Application import Application @@ -9,13 +10,17 @@ from UM.Application import Application class BuildPlateModel(ListModel): maxBuildPlateChanged = pyqtSignal() activeBuildPlateChanged = pyqtSignal() + selectionChanged = pyqtSignal() def __init__(self): super().__init__() Application.getInstance().getController().getScene().sceneChanged.connect(self.updateMaxBuildPlate) # it may be a bit inefficient when changing a lot simultaneously + Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbers) + Selection.selectionChanged.connect(self._updateSelectedObjectBuildPlateNumbers) self._max_build_plate = 1 # default self._active_build_plate = -1 + self._selection_build_plates = [] @pyqtSlot(int) def setActiveBuildPlate(self, nr): @@ -60,3 +65,14 @@ class BuildPlateModel(ListModel): @staticmethod def createBuildPlateModel(): return BuildPlateModel() + + def _updateSelectedObjectBuildPlateNumbers(self, *args): + result = set() + for node in Selection.getAllSelectedObjects(): + result.add(node.callDecoration("getBuildPlateNumber")) + self._selection_build_plates = list(result) + self.selectionChanged.emit() + + @pyqtProperty("QVariantList", notify = selectionChanged) + def selectionBuildPlates(self): + return self._selection_build_plates diff --git a/resources/qml/Menus/ContextMenu.qml b/resources/qml/Menus/ContextMenu.qml index a80de2d8a7..910f0a951a 100644 --- a/resources/qml/Menus/ContextMenu.qml +++ b/resources/qml/Menus/ContextMenu.qml @@ -49,7 +49,7 @@ Menu text: Cura.BuildPlateModel.getItem(index).name; onTriggered: CuraActions.setBuildPlateForSelection(Cura.BuildPlateModel.getItem(index).buildPlateNumber); checkable: true - checked: Cura.BuildPlateModel.getItem(index).buildPlateNumber == Cura.BuildPlateModel.activeBuildPlate + checked: Cura.BuildPlateModel.selectionBuildPlates.indexOf(Cura.BuildPlateModel.getItem(index).buildPlateNumber) != -1; visible: UM.Preferences.getValue("cura/use_multi_build_plate") } onObjectAdded: base.insertItem(index, object); @@ -57,7 +57,10 @@ Menu } MenuItem { text: "New build plate"; - onTriggered: CuraActions.setBuildPlateForSelection(Cura.BuildPlateModel.maxBuildPlate + 1); + onTriggered: { + CuraActions.setBuildPlateForSelection(Cura.BuildPlateModel.maxBuildPlate + 1); + checked = false; + } checkable: true checked: false visible: UM.Preferences.getValue("cura/use_multi_build_plate") diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index b67c0c0bec..ac99d6b0ef 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -56,7 +56,7 @@ Rectangle sourceSize.width: width sourceSize.height: width color: UM.Theme.getColor("setting_control_text") - source: collapsed ? UM.Theme.getIcon("arrow_top") : UM.Theme.getIcon("arrow_bottom") + source: collapsed ? UM.Theme.getIcon("arrow_left") : UM.Theme.getIcon("arrow_bottom") } label: Label{ } } From 24ad68aeb5b1c28f47ac81dd320939a75092c291 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 21 Dec 2017 15:13:12 +0100 Subject: [PATCH 26/42] Fix initial state of 'See only current build plate'. CURA-4525 --- resources/qml/ObjectsList.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index ac99d6b0ef..e688928570 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -142,7 +142,7 @@ Rectangle { id: filterBuildPlateCheckbox visible: !collapsed - checked: boolCheck(UM.Preferences.getValue("view/filter_current_build_plate")) + checked: UM.Preferences.getValue("view/filter_current_build_plate") onClicked: UM.Preferences.setValue("view/filter_current_build_plate", checked) text: catalog.i18nc("@option:check","See only current build plate"); From 9ff15bf72d49f24cd77613f5830f949aa0662e11 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 21 Dec 2017 15:42:23 +0100 Subject: [PATCH 27/42] Fixed not always updating objects list when changing build plate number by adding signals. CURA-4525 --- cura/Scene/BuildPlateDecorator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index cfbe792699..6a288b4bcd 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -14,9 +14,13 @@ class BuildPlateDecorator(SceneNodeDecorator): # 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 is not None: + self._node.transformationChanged.emit() + #self._node.transformationChanged.emit() if self._node and self._node.callDecoration("isGroup"): for child in self._node.getChildren(): child.callDecoration("setBuildPlateNumber", nr) + child.transformationChanged.emit() def getBuildPlateNumber(self): return self._build_plate_number From 2c831ceb05fe46993ebcbd81ec28b7dc8e232faa Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 21 Dec 2017 15:47:40 +0100 Subject: [PATCH 28/42] For this version, send all build plates. No print view when multiple build plates. CURA-4525 --- plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py index 1b029903b7..05069d1c0d 100644 --- a/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/NetworkClusterPrinterOutputDevice.py @@ -259,7 +259,7 @@ class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinte self._add_build_plate_number = len(self._job_list) > 1 self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer - if len(self._printers) > 1 or len(gcodes) > 1: + if len(self._printers) > 1: self.spawnPrintView() # Ask user how to print it. elif len(self._printers) == 1: # If there is only one printer, don't bother asking. From 663ceab0699d1bae2aaab8895d81dbdc3cd0e813 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 21 Dec 2017 16:53:43 +0100 Subject: [PATCH 29/42] Undo emit transformationChanges (caused a crash), themed objects in objects list. CURA-4525 --- cura/ArrangeObjectsAllBuildPlatesJob.py | 2 -- cura/BuildPlateModel.py | 6 +++++- cura/Scene/BuildPlateDecorator.py | 10 +++++----- resources/qml/Actions.qml | 1 - resources/qml/ObjectsList.qml | 10 ++++++---- resources/themes/cura-light/theme.json | 5 +++-- 6 files changed, 19 insertions(+), 15 deletions(-) diff --git a/cura/ArrangeObjectsAllBuildPlatesJob.py b/cura/ArrangeObjectsAllBuildPlatesJob.py index 7991ac39f0..f062c2b23b 100644 --- a/cura/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/ArrangeObjectsAllBuildPlatesJob.py @@ -34,8 +34,6 @@ class ArrangeArray: 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 diff --git a/cura/BuildPlateModel.py b/cura/BuildPlateModel.py index c832a7c522..118a5ce271 100644 --- a/cura/BuildPlateModel.py +++ b/cura/BuildPlateModel.py @@ -40,7 +40,11 @@ class BuildPlateModel(ListModel): def maxBuildPlate(self): return self._max_build_plate - def updateMaxBuildPlate(self, source): + def updateMaxBuildPlate(self, *args): + if args: + source = args[0] + else: + source = None if not issubclass(type(source), SceneNode): return max_build_plate = self._calcMaxBuildPlate() diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index 6a288b4bcd..36b89c5e5d 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -1,6 +1,5 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator -from UM.Application import Application -from UM.Logger import Logger +from UM.Scene.SceneNode import SceneNode ## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator. @@ -14,13 +13,14 @@ class BuildPlateDecorator(SceneNodeDecorator): # 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 is not None: - self._node.transformationChanged.emit() + # if issubclass(type(self._node), SceneNode): # TODO: Crashes on ArrangeObjectsAllBuildPlatesJob + # self._node.transformationChanged.emit() #self._node.transformationChanged.emit() if self._node and self._node.callDecoration("isGroup"): for child in self._node.getChildren(): child.callDecoration("setBuildPlateNumber", nr) - child.transformationChanged.emit() + # if issubclass(type(child), SceneNode): + # child.transformationChanged.emit() def getBuildPlateNumber(self): return self._build_plate_number diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index 0727642d7e..93861c0963 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -316,7 +316,6 @@ Item { id: arrangeAllBuildPlatesAction; text: catalog.i18nc("@action:inmenu menubar:edit","Arrange All Models To All Build Plates"); - //iconName: "document-open"; onTriggered: Printer.arrangeObjectsToAllBuildPlates(); } diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index e688928570..761ff6d3f5 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -146,6 +146,7 @@ Rectangle onClicked: UM.Preferences.setValue("view/filter_current_build_plate", checked) text: catalog.i18nc("@option:check","See only current build plate"); + style: UM.Theme.styles.checkbox; anchors { @@ -192,6 +193,7 @@ Rectangle frameVisible: true height: UM.Theme.getSize("build_plate_selection_size").height width: parent.width - 2 * UM.Theme.getSize("default_margin").height + style: UM.Theme.styles.scrollview anchors { @@ -222,9 +224,8 @@ Rectangle { 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 + style: UM.Theme.styles.sidebar_action_button + height: UM.Theme.getSize("objects_menu_button").height; tooltip: ''; anchors { @@ -244,7 +245,8 @@ Rectangle { id: arrangeBuildPlateButton; text: catalog.i18nc("@action:button","Arrange current build plate"); - height: 25 + style: UM.Theme.styles.sidebar_action_button + height: UM.Theme.getSize("objects_menu_button").height; tooltip: ''; anchors { diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 714e578d97..18f2650e77 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -393,8 +393,9 @@ "jobspecs_line": [2.0, 2.0], - "objects_menu_size": [17, 40], + "objects_menu_size": [20, 40], "objects_menu_size_collapsed": [15, 15], - "build_plate_selection_size": [15, 5] + "build_plate_selection_size": [15, 5], + "objects_menu_button": [0.3, 2.7] } } From c8243a0dddd1fdc07edea8240c0de990a979fb26 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 3 Jan 2018 12:47:09 +0100 Subject: [PATCH 30/42] CURA-4525 fix convex hull, changed size of object list --- cura/ConvexHullNode.py | 4 ++-- resources/themes/cura-light/theme.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cura/ConvexHullNode.py b/cura/ConvexHullNode.py index cec9d3d698..02d8ed833c 100644 --- a/cura/ConvexHullNode.py +++ b/cura/ConvexHullNode.py @@ -64,8 +64,8 @@ class ConvexHullNode(SceneNode): ConvexHullNode.shader.setUniformValue("u_diffuseColor", self._color) ConvexHullNode.shader.setUniformValue("u_opacity", 0.6) - if self.getParent() and self.getParent().callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate: - if self.getMeshData(): + if self.getParent(): + if self.getMeshData() and issubclass(type(self._node), SceneNode) and self._node.callDecoration("getBuildPlateNumber") == Application.getInstance().getBuildPlateModel().activeBuildPlate: renderer.queueNode(self, transparent = True, shader = ConvexHullNode.shader, backface_cull = True, sort = -8) if self._convex_hull_head_mesh: renderer.queueNode(self, shader = ConvexHullNode.shader, transparent = True, mesh = self._convex_hull_head_mesh, backface_cull = True, sort = -8) diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 18f2650e77..5b3e2f019a 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -394,7 +394,7 @@ "jobspecs_line": [2.0, 2.0], "objects_menu_size": [20, 40], - "objects_menu_size_collapsed": [15, 15], + "objects_menu_size_collapsed": [15, 17], "build_plate_selection_size": [15, 5], "objects_menu_button": [0.3, 2.7] } From cb1484ee63f9cba6f2210bdc3232b7af5e31f9bd Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 3 Jan 2018 12:52:32 +0100 Subject: [PATCH 31/42] CURA-4525 update size and margins of objects list --- resources/qml/Cura.qml | 4 ---- resources/themes/cura-light/theme.json | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index ec9773679a..39834b88b3 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -383,10 +383,6 @@ UM.MainWindow { bottom: parent.bottom; left: parent.left; - leftMargin: UM.Theme.getSize("default_margin").width; - rightMargin: UM.Theme.getSize("default_margin").width; - topMargin: UM.Theme.getSize("default_margin").height; - bottomMargin: UM.Theme.getSize("default_margin").height; } } diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 5b3e2f019a..53bef1e7d9 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -394,7 +394,7 @@ "jobspecs_line": [2.0, 2.0], "objects_menu_size": [20, 40], - "objects_menu_size_collapsed": [15, 17], + "objects_menu_size_collapsed": [20, 17], "build_plate_selection_size": [15, 5], "objects_menu_button": [0.3, 2.7] } From dd989a1a51be0854a47515500d016c13bda39ae6 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 3 Jan 2018 13:43:09 +0100 Subject: [PATCH 32/42] CURA-4525 refresh objects list of all items after changing build plate --- cura/Scene/BuildPlateDecorator.py | 9 +++------ cura/Scene/CuraSceneNode.py | 3 +++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index 36b89c5e5d..44372976f0 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -1,5 +1,5 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator -from UM.Scene.SceneNode import SceneNode +from cura.Scene.CuraSceneNode import CuraSceneNode ## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator. @@ -13,14 +13,11 @@ class BuildPlateDecorator(SceneNodeDecorator): # Make sure that groups are set correctly # setBuildPlateForSelection in CuraActions makes sure that no single childs are set. self._build_plate_number = nr - # if issubclass(type(self._node), SceneNode): # TODO: Crashes on ArrangeObjectsAllBuildPlatesJob - # self._node.transformationChanged.emit() - #self._node.transformationChanged.emit() + if issubclass(type(self._node), CuraSceneNode): + self._node.transformChanged() # trigger refresh node without introducing a new signal if self._node and self._node.callDecoration("isGroup"): for child in self._node.getChildren(): child.callDecoration("setBuildPlateNumber", nr) - # if issubclass(type(child), SceneNode): - # child.transformationChanged.emit() def getBuildPlateNumber(self): return self._build_plate_number diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index e68405daf6..9df2931f0b 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -38,3 +38,6 @@ class CuraSceneNode(SceneNode): copy.addChild(deepcopy(child, memo)) self.calculateBoundingBoxMesh() return copy + + def transformChanged(self) -> None: + self._transformChanged() From 2f965cc05338625cfbd4326d2bfb5978e9d3e3db Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 3 Jan 2018 14:18:40 +0100 Subject: [PATCH 33/42] CURA-4525 switch locations of build plates and objects list --- resources/qml/ObjectsList.qml | 128 +++++++++++++++++----------------- 1 file changed, 65 insertions(+), 63 deletions(-) diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index 761ff6d3f5..a924959581 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -62,6 +62,70 @@ Rectangle } } + Component { + id: buildPlateDelegate + Rectangle + { + height: childrenRect.height + color: Cura.BuildPlateModel.getItem(index).buildPlateNumber == Cura.BuildPlateModel.activeBuildPlate ? palette.highlight : index % 2 ? palette.base : palette.alternateBase + width: parent.width + Label + { + id: buildPlateNameLabel + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30 + text: Cura.BuildPlateModel.getItem(index) ? Cura.BuildPlateModel.getItem(index).name : ""; + color: Cura.BuildPlateModel.activeBuildPlate == index ? palette.highlightedText : palette.text + elide: Text.ElideRight + } + + MouseArea + { + anchors.fill: parent; + onClicked: + { + Cura.BuildPlateModel.setActiveBuildPlate(index); + } + } + } + } + + ScrollView + { + id: buildPlateSelection + frameVisible: true + height: UM.Theme.getSize("build_plate_selection_size").height + width: parent.width - 2 * UM.Theme.getSize("default_margin").height + style: UM.Theme.styles.scrollview + + anchors + { + top: collapseButton.bottom; + topMargin: UM.Theme.getSize("default_margin").height; + left: parent.left; + leftMargin: UM.Theme.getSize("default_margin").height; + //bottom: objectsList.top; + bottomMargin: UM.Theme.getSize("default_margin").height; + } + + Rectangle + { + parent: viewport + anchors.fill: parent + color: palette.light + } + + ListView + { + id: buildPlateListView + model: Cura.BuildPlateModel + width: parent.width + delegate: buildPlateDelegate + } + } + + Component { id: objectDelegate Rectangle @@ -113,7 +177,7 @@ Rectangle anchors { - top: collapseButton.bottom; + top: buildPlateSelection.bottom; topMargin: UM.Theme.getSize("default_margin").height; left: parent.left; leftMargin: UM.Theme.getSize("default_margin").height; @@ -154,69 +218,7 @@ Rectangle topMargin: UM.Theme.getSize("default_margin").height; bottomMargin: UM.Theme.getSize("default_margin").height; leftMargin: UM.Theme.getSize("default_margin").height; - bottom: buildPlateSelection.top; - } - } - - Component { - id: buildPlateDelegate - Rectangle - { - height: childrenRect.height - color: Cura.BuildPlateModel.getItem(index).buildPlateNumber == Cura.BuildPlateModel.activeBuildPlate ? palette.highlight : index % 2 ? palette.base : palette.alternateBase - width: parent.width - Label - { - id: buildPlateNameLabel - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width - width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30 - text: Cura.BuildPlateModel.getItem(index) ? Cura.BuildPlateModel.getItem(index).name : ""; - color: Cura.BuildPlateModel.activeBuildPlate == index ? palette.highlightedText : palette.text - elide: Text.ElideRight - } - - MouseArea - { - anchors.fill: parent; - onClicked: - { - Cura.BuildPlateModel.setActiveBuildPlate(index); - } - } - } - } - - ScrollView - { - id: buildPlateSelection - frameVisible: true - height: UM.Theme.getSize("build_plate_selection_size").height - width: parent.width - 2 * UM.Theme.getSize("default_margin").height - style: UM.Theme.styles.scrollview - - anchors - { - topMargin: UM.Theme.getSize("default_margin").height; - left: parent.left; - leftMargin: UM.Theme.getSize("default_margin").height; bottom: arrangeAllBuildPlatesButton.top; - bottomMargin: UM.Theme.getSize("default_margin").height; - } - - Rectangle - { - parent: viewport - anchors.fill: parent - color: palette.light - } - - ListView - { - id: buildPlateListView - model: Cura.BuildPlateModel - width: parent.width - delegate: buildPlateDelegate } } From a5630e5c54ef65e5dcd47d9a99cd1bb92da1acc6 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 3 Jan 2018 15:05:06 +0100 Subject: [PATCH 34/42] Move all arranging related code into a subfolder --- cura/{ => Arranging}/Arrange.py | 0 .../ArrangeObjectsAllBuildPlatesJob.py | 4 +--- cura/{ => Arranging}/ArrangeObjectsJob.py | 3 +-- cura/Arranging/__init__.py | 0 cura/CuraApplication.py | 10 +++------- cura/MultiplyObjectsJob.py | 12 +----------- tests/TestArrange.py | 4 +--- 7 files changed, 7 insertions(+), 26 deletions(-) rename cura/{ => Arranging}/Arrange.py (100%) mode change 100755 => 100644 rename cura/{ => Arranging}/ArrangeObjectsAllBuildPlatesJob.py (97%) rename cura/{ => Arranging}/ArrangeObjectsJob.py (97%) mode change 100755 => 100644 create mode 100644 cura/Arranging/__init__.py diff --git a/cura/Arrange.py b/cura/Arranging/Arrange.py old mode 100755 new mode 100644 similarity index 100% rename from cura/Arrange.py rename to cura/Arranging/Arrange.py diff --git a/cura/ArrangeObjectsAllBuildPlatesJob.py b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py similarity index 97% rename from cura/ArrangeObjectsAllBuildPlatesJob.py rename to cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py index f062c2b23b..6409146ca8 100644 --- a/cura/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py @@ -4,16 +4,14 @@ 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.Arranging.Arrange import Arrange from cura.ShapeArray import ShapeArray from typing import List diff --git a/cura/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py old mode 100755 new mode 100644 similarity index 97% rename from cura/ArrangeObjectsJob.py rename to cura/Arranging/ArrangeObjectsJob.py index d650fd7f57..24db529fc1 --- a/cura/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -4,7 +4,6 @@ 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 @@ -13,7 +12,7 @@ from UM.i18n import i18nCatalog i18n_catalog = i18nCatalog("cura") from cura.ZOffsetDecorator import ZOffsetDecorator -from cura.Arrange import Arrange +from cura.Arranging.Arrange import Arrange from cura.ShapeArray import ShapeArray from typing import List diff --git a/cura/Arranging/__init__.py b/cura/Arranging/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 68213cfdca..51ec131b4a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -17,7 +17,6 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Mesh.ReadMeshJob import ReadMeshJob from UM.Logger import Logger from UM.Preferences import Preferences -from UM.SaveFile import SaveFile from UM.Scene.Selection import Selection from UM.Scene.GroupDecorator import GroupDecorator from UM.Settings.ContainerStack import ContainerStack @@ -33,7 +32,7 @@ from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.SetTransformOperation import SetTransformOperation -from cura.Arrange import Arrange +from cura.Arranging.Arrange import Arrange from cura.ShapeArray import ShapeArray from cura.ConvexHullDecorator import ConvexHullDecorator from cura.SetParentOperation import SetParentOperation @@ -42,8 +41,8 @@ from cura.BlockSlicingDecorator import BlockSlicingDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.Scene.CuraSceneNode import CuraSceneNode -from cura.ArrangeObjectsJob import ArrangeObjectsJob -from cura.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob +from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob +from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob from cura.MultiplyObjectsJob import MultiplyObjectsJob from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType @@ -77,8 +76,6 @@ from cura.Settings.ContainerSettingsModel import ContainerSettingsModel from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler from cura.Settings.QualitySettingsModel import QualitySettingsModel from cura.Settings.ContainerManager import ContainerManager -from cura.Settings.GlobalStack import GlobalStack -from cura.Settings.ExtruderStack import ExtruderStack from cura.ObjectManager import ObjectManager from cura.BuildPlateModel import BuildPlateModel @@ -93,7 +90,6 @@ import sys import os.path import numpy import copy -import urllib.parse import os import argparse import json diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 63a38993a2..f724c90c62 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -2,25 +2,15 @@ # 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.Scene.BuildPlateDecorator import BuildPlateDecorator -from cura.Arrange import Arrange +from cura.Arranging.Arrange import Arrange from cura.ShapeArray import ShapeArray -from typing import List - from UM.Application import Application -from UM.Scene.Selection import Selection from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation diff --git a/tests/TestArrange.py b/tests/TestArrange.py index f3612c1ac7..1da8ff7ba8 100755 --- a/tests/TestArrange.py +++ b/tests/TestArrange.py @@ -1,8 +1,6 @@ -import pytest import numpy -import time -from cura.Arrange import Arrange +from cura.Arranging.Arrange import Arrange from cura.ShapeArray import ShapeArray From 62b06b063b8e0228d4b2ce28fe7823400c1bd101 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 3 Jan 2018 15:06:20 +0100 Subject: [PATCH 35/42] Also move other operations into operations subfolder --- cura/CuraActions.py | 2 +- cura/CuraApplication.py | 2 +- cura/{ => Operations}/PlatformPhysicsOperation.py | 0 cura/{ => Operations}/SetParentOperation.py | 0 cura/Operations/__init__.py | 0 cura/PlatformPhysics.py | 2 +- 6 files changed, 3 insertions(+), 3 deletions(-) rename cura/{ => Operations}/PlatformPhysicsOperation.py (100%) rename cura/{ => Operations}/SetParentOperation.py (100%) create mode 100644 cura/Operations/__init__.py diff --git a/cura/CuraActions.py b/cura/CuraActions.py index dbcd31f646..28e13e96b7 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -14,7 +14,7 @@ from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.SetTransformOperation import SetTransformOperation -from cura.SetParentOperation import SetParentOperation +from cura.Operations.SetParentOperation import SetParentOperation from cura.MultiplyObjectsJob import MultiplyObjectsJob from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation from cura.Settings.ExtruderManager import ExtruderManager diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 51ec131b4a..4790f655f7 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -35,7 +35,7 @@ from UM.Operations.SetTransformOperation import SetTransformOperation from cura.Arranging.Arrange import Arrange from cura.ShapeArray import ShapeArray from cura.ConvexHullDecorator import ConvexHullDecorator -from cura.SetParentOperation import SetParentOperation +from cura.Operations.SetParentOperation import SetParentOperation from cura.SliceableObjectDecorator import SliceableObjectDecorator from cura.BlockSlicingDecorator import BlockSlicingDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator diff --git a/cura/PlatformPhysicsOperation.py b/cura/Operations/PlatformPhysicsOperation.py similarity index 100% rename from cura/PlatformPhysicsOperation.py rename to cura/Operations/PlatformPhysicsOperation.py diff --git a/cura/SetParentOperation.py b/cura/Operations/SetParentOperation.py similarity index 100% rename from cura/SetParentOperation.py rename to cura/Operations/SetParentOperation.py diff --git a/cura/Operations/__init__.py b/cura/Operations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 7aec519e6f..933d7c8608 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -12,7 +12,7 @@ from UM.Preferences import Preferences from cura.ConvexHullDecorator import ConvexHullDecorator -from . import PlatformPhysicsOperation +from cura.Operations import PlatformPhysicsOperation from . import ZOffsetDecorator import random # used for list shuffling From 08322d0a5ead26cfdc57d4ba168adf30b252b8aa Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 3 Jan 2018 15:09:19 +0100 Subject: [PATCH 36/42] Move all decorators in the scene subfolder --- cura/Arranging/Arrange.py | 4 ++-- cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py | 4 ++-- cura/Arranging/ArrangeObjectsJob.py | 4 ++-- cura/{ => Arranging}/ShapeArray.py | 0 cura/CuraApplication.py | 10 +++++----- cura/MultiplyObjectsJob.py | 2 +- cura/PlatformPhysics.py | 4 ++-- cura/{ => Scene}/BlockSlicingDecorator.py | 0 cura/{ => Scene}/ConvexHullDecorator.py | 2 +- cura/{ => Scene}/ConvexHullNode.py | 0 cura/{ => Scene}/GCodeListDecorator.py | 0 cura/{ => Scene}/SliceableObjectDecorator.py | 0 cura/{ => Scene}/ZOffsetDecorator.py | 0 cura/Scene/__init__.py | 0 plugins/3MFReader/ThreeMFReader.py | 5 ++--- plugins/GCodeReader/FlavorParser.py | 2 +- plugins/SimulationView/SimulationView.py | 2 +- tests/TestArrange.py | 2 +- 18 files changed, 20 insertions(+), 21 deletions(-) rename cura/{ => Arranging}/ShapeArray.py (100%) mode change 100755 => 100644 rename cura/{ => Scene}/BlockSlicingDecorator.py (100%) rename cura/{ => Scene}/ConvexHullDecorator.py (99%) rename cura/{ => Scene}/ConvexHullNode.py (100%) rename cura/{ => Scene}/GCodeListDecorator.py (100%) rename cura/{ => Scene}/SliceableObjectDecorator.py (100%) rename cura/{ => Scene}/ZOffsetDecorator.py (100%) create mode 100644 cura/Scene/__init__.py diff --git a/cura/Arranging/Arrange.py b/cura/Arranging/Arrange.py index 7691853f95..5ac0c09dc4 100644 --- a/cura/Arranging/Arrange.py +++ b/cura/Arranging/Arrange.py @@ -1,8 +1,8 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Logger import Logger from UM.Math.Vector import Vector -from cura.ShapeArray import ShapeArray -from cura import ZOffsetDecorator +from cura.Arranging.ShapeArray import ShapeArray +from cura.Scene import ZOffsetDecorator from collections import namedtuple diff --git a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py index 6409146ca8..859ad481d6 100644 --- a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py @@ -10,9 +10,9 @@ from UM.Message import Message from UM.i18n import i18nCatalog i18n_catalog = i18nCatalog("cura") -from cura.ZOffsetDecorator import ZOffsetDecorator +from cura.Scene.ZOffsetDecorator import ZOffsetDecorator from cura.Arranging.Arrange import Arrange -from cura.ShapeArray import ShapeArray +from cura.Arranging.ShapeArray import ShapeArray from typing import List diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 24db529fc1..f529543779 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -11,9 +11,9 @@ from UM.Message import Message from UM.i18n import i18nCatalog i18n_catalog = i18nCatalog("cura") -from cura.ZOffsetDecorator import ZOffsetDecorator +from cura.Scene.ZOffsetDecorator import ZOffsetDecorator from cura.Arranging.Arrange import Arrange -from cura.ShapeArray import ShapeArray +from cura.Arranging.ShapeArray import ShapeArray from typing import List diff --git a/cura/ShapeArray.py b/cura/Arranging/ShapeArray.py old mode 100755 new mode 100644 similarity index 100% rename from cura/ShapeArray.py rename to cura/Arranging/ShapeArray.py diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 4790f655f7..42bd70fdc8 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -33,11 +33,11 @@ from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.SetTransformOperation import SetTransformOperation from cura.Arranging.Arrange import Arrange -from cura.ShapeArray import ShapeArray -from cura.ConvexHullDecorator import ConvexHullDecorator +from cura.Arranging.ShapeArray import ShapeArray +from cura.Scene.ConvexHullDecorator import ConvexHullDecorator from cura.Operations.SetParentOperation import SetParentOperation -from cura.SliceableObjectDecorator import SliceableObjectDecorator -from cura.BlockSlicingDecorator import BlockSlicingDecorator +from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator +from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.Scene.CuraSceneNode import CuraSceneNode @@ -62,7 +62,7 @@ from . import BuildVolume from . import CameraAnimation from . import PrintInformation from . import CuraActions -from . import ZOffsetDecorator +from cura.Scene import ZOffsetDecorator from . import CuraSplashScreen from . import CameraImageProvider from . import MachineActionManager diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index f724c90c62..441d4c96c3 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -8,7 +8,7 @@ from UM.i18n import i18nCatalog i18n_catalog = i18nCatalog("cura") from cura.Arranging.Arrange import Arrange -from cura.ShapeArray import ShapeArray +from cura.Arranging.ShapeArray import ShapeArray from UM.Application import Application from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 933d7c8608..06d796eed5 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -10,10 +10,10 @@ from UM.Math.Vector import Vector from UM.Scene.Selection import Selection from UM.Preferences import Preferences -from cura.ConvexHullDecorator import ConvexHullDecorator +from cura.Scene.ConvexHullDecorator import ConvexHullDecorator from cura.Operations import PlatformPhysicsOperation -from . import ZOffsetDecorator +from cura.Scene import ZOffsetDecorator import random # used for list shuffling diff --git a/cura/BlockSlicingDecorator.py b/cura/Scene/BlockSlicingDecorator.py similarity index 100% rename from cura/BlockSlicingDecorator.py rename to cura/Scene/BlockSlicingDecorator.py diff --git a/cura/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py similarity index 99% rename from cura/ConvexHullDecorator.py rename to cura/Scene/ConvexHullDecorator.py index 50fa8ce7f6..3a563c2764 100644 --- a/cura/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -7,7 +7,7 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Settings.ContainerRegistry import ContainerRegistry from cura.Settings.ExtruderManager import ExtruderManager -from . import ConvexHullNode +from cura.Scene import ConvexHullNode import numpy diff --git a/cura/ConvexHullNode.py b/cura/Scene/ConvexHullNode.py similarity index 100% rename from cura/ConvexHullNode.py rename to cura/Scene/ConvexHullNode.py diff --git a/cura/GCodeListDecorator.py b/cura/Scene/GCodeListDecorator.py similarity index 100% rename from cura/GCodeListDecorator.py rename to cura/Scene/GCodeListDecorator.py diff --git a/cura/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py similarity index 100% rename from cura/SliceableObjectDecorator.py rename to cura/Scene/SliceableObjectDecorator.py diff --git a/cura/ZOffsetDecorator.py b/cura/Scene/ZOffsetDecorator.py similarity index 100% rename from cura/ZOffsetDecorator.py rename to cura/Scene/ZOffsetDecorator.py diff --git a/cura/Scene/__init__.py b/cura/Scene/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 30b62b59aa..727bce2112 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -4,7 +4,6 @@ import os.path import zipfile -from UM.Job import Job from UM.Logger import Logger from UM.Math.Matrix import Matrix from UM.Math.Vector import Vector @@ -17,8 +16,8 @@ from cura.Settings.ExtruderManager import ExtruderManager from cura.QualityManager import QualityManager from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.BuildPlateDecorator import BuildPlateDecorator -from cura.SliceableObjectDecorator import SliceableObjectDecorator -from cura.ZOffsetDecorator import ZOffsetDecorator +from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator +from cura.Scene.ZOffsetDecorator import ZOffsetDecorator MYPY = False diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index cd317027ee..fa5d6da243 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -17,7 +17,7 @@ catalog = i18nCatalog("cura") from cura import LayerDataBuilder from cura import LayerDataDecorator from cura.LayerPolygon import LayerPolygon -from cura.GCodeListDecorator import GCodeListDecorator +from cura.Scene.GCodeListDecorator import GCodeListDecorator from cura.Settings.ExtruderManager import ExtruderManager import numpy diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 6fc362725e..7a716d3b2b 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -25,7 +25,7 @@ from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGLContext import OpenGLContext from UM.View.View import View from UM.i18n import i18nCatalog -from cura.ConvexHullNode import ConvexHullNode +from cura.Scene.ConvexHullNode import ConvexHullNode from cura.CuraApplication import CuraApplication from .NozzleNode import NozzleNode diff --git a/tests/TestArrange.py b/tests/TestArrange.py index 1da8ff7ba8..737305f638 100755 --- a/tests/TestArrange.py +++ b/tests/TestArrange.py @@ -1,7 +1,7 @@ import numpy from cura.Arranging.Arrange import Arrange -from cura.ShapeArray import ShapeArray +from cura.Arranging.ShapeArray import ShapeArray def gimmeShapeArray(): From e7e5729006c244a3cc1fec88bb1f659eeeabc8e9 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 3 Jan 2018 15:21:56 +0100 Subject: [PATCH 37/42] CURA-4525 deselect after move to build plate and build plate change --- cura/BuildPlateModel.py | 1 + cura/CuraActions.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/cura/BuildPlateModel.py b/cura/BuildPlateModel.py index 118a5ce271..2d558a91b9 100644 --- a/cura/BuildPlateModel.py +++ b/cura/BuildPlateModel.py @@ -28,6 +28,7 @@ class BuildPlateModel(ListModel): return Logger.log("d", "Select build plate: %s" % nr) self._active_build_plate = nr + Selection.clear() self.activeBuildPlateChanged.emit() diff --git a/cura/CuraActions.py b/cura/CuraActions.py index dbcd31f646..a55ef66e01 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -153,5 +153,7 @@ class CuraActions(QObject): operation.addOperation(SetBuildPlateNumberOperation(node, build_plate_nr)) operation.push() + Selection.clear() + def _openUrl(self, url): QDesktopServices.openUrl(url) From 62487e8ea2dc3603bffaa76989b79179342d46ee Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 3 Jan 2018 15:41:08 +0100 Subject: [PATCH 38/42] CURA-4525 cleanups and change comment and added new-lines --- cura/Operations/SetBuildPlateNumberOperation.py | 2 +- plugins/CuraEngineBackend/CuraEngineBackend.py | 13 ------------- plugins/CuraEngineBackend/ProcessSlicedLayersJob.py | 2 -- plugins/CuraEngineBackend/StartSliceJob.py | 1 - plugins/ImageReader/ImageReader.py | 1 - plugins/X3DReader/X3DReader.py | 1 - resources/qml/Menus/ContextMenu.qml | 2 ++ 7 files changed, 3 insertions(+), 19 deletions(-) diff --git a/cura/Operations/SetBuildPlateNumberOperation.py b/cura/Operations/SetBuildPlateNumberOperation.py index bbef4caf84..c14d737f93 100644 --- a/cura/Operations/SetBuildPlateNumberOperation.py +++ b/cura/Operations/SetBuildPlateNumberOperation.py @@ -6,7 +6,7 @@ from UM.Operations.Operation import Operation from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -## Simple operation to set the extruder a certain object should be printed with. +## Simple operation to set the buildplate number of a scenenode. class SetBuildPlateNumberOperation(Operation): def __init__(self, node: SceneNode, build_plate_nr: int) -> None: self._node = node diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 473dbee31a..74dd515951 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -357,19 +357,6 @@ class CuraEngineBackend(QObject, Backend): else: self.backendStateChange.emit(BackendState.NotStarted) - # Doesn't occur anymore, is handled in slice() - # if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice: - # if Application.getInstance().platformActivity: - # self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."), - # title = catalog.i18nc("@info:title", "Unable to slice")) - # self._error_message.show() - # #self.backendStateChange.emit(BackendState.Error) - # else: - # #self.backendStateChange.emit(BackendState.NotStarted) - # pass - # self._invokeSlice() - # return - # Preparation completed, send it to the backend. self._socket.sendMessage(job.getSliceMessage()) diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 5f632768ec..be9c3f73f0 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -4,7 +4,6 @@ import gc from UM.Job import Job -from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode from UM.Application import Application from UM.Mesh.MeshData import MeshData @@ -22,7 +21,6 @@ from cura.Settings.ExtruderManager import ExtruderManager from cura import LayerDataBuilder from cura import LayerDataDecorator from cura import LayerPolygon -# from cura.Scene.CuraSceneNode import CuraSceneNode import numpy from time import time diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 9a61c4c8a2..a9618c5472 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -10,7 +10,6 @@ from UM.Job import Job from UM.Application import Application from UM.Logger import Logger -#from UM.Scene.SceneNode import SceneNode from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Settings.Validator import ValidatorState diff --git a/plugins/ImageReader/ImageReader.py b/plugins/ImageReader/ImageReader.py index 2529abf2d8..3a98abccf5 100644 --- a/plugins/ImageReader/ImageReader.py +++ b/plugins/ImageReader/ImageReader.py @@ -8,7 +8,6 @@ from PyQt5.QtCore import Qt from UM.Mesh.MeshReader import MeshReader from UM.Mesh.MeshBuilder import MeshBuilder -#from UM.Scene.SceneNode import SceneNode from UM.Math.Vector import Vector from UM.Job import Job from UM.Logger import Logger diff --git a/plugins/X3DReader/X3DReader.py b/plugins/X3DReader/X3DReader.py index 883ed7b0b6..b0b9e00a5b 100644 --- a/plugins/X3DReader/X3DReader.py +++ b/plugins/X3DReader/X3DReader.py @@ -11,7 +11,6 @@ from UM.Math.Matrix import Matrix from UM.Math.Vector import Vector from UM.Mesh.MeshBuilder import MeshBuilder from UM.Mesh.MeshReader import MeshReader -#from UM.Scene.SceneNode import SceneNode from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode MYPY = False diff --git a/resources/qml/Menus/ContextMenu.qml b/resources/qml/Menus/ContextMenu.qml index 910f0a951a..1a4b421572 100644 --- a/resources/qml/Menus/ContextMenu.qml +++ b/resources/qml/Menus/ContextMenu.qml @@ -42,6 +42,7 @@ Menu MenuSeparator { visible: UM.Preferences.getValue("cura/use_multi_build_plate") } + Instantiator { model: Cura.BuildPlateModel @@ -55,6 +56,7 @@ Menu onObjectAdded: base.insertItem(index, object); onObjectRemoved: base.removeItem(object); } + MenuItem { text: "New build plate"; onTriggered: { From 9e5f0e10b968daedfc2ec2e96e9292c55f095cd2 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 3 Jan 2018 15:49:31 +0100 Subject: [PATCH 39/42] CURA-4525 add comment and type hint --- cura/Arranging/Arrange.py | 1 + cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/cura/Arranging/Arrange.py b/cura/Arranging/Arrange.py index 5ac0c09dc4..a90a97c3c2 100644 --- a/cura/Arranging/Arrange.py +++ b/cura/Arranging/Arrange.py @@ -169,6 +169,7 @@ class Arrange: # \param x x-coordinate # \param y y-coordinate # \param shape_arr ShapeArray object + # \param update_empty updates the _is_empty, used when adding disallowed areas def place(self, x, y, shape_arr, update_empty = True): x = int(self._scale * x) y = int(self._scale * y) diff --git a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py index 859ad481d6..3f23b0dbe7 100644 --- a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py @@ -18,7 +18,7 @@ from typing import List class ArrangeArray: - def __init__(self, x, y, fixed_nodes): + def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]): self._x = x self._y = y self._fixed_nodes = fixed_nodes From 8c7a0d4a8e70ddb722943b709c2d2fc9d0dccc2d Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 4 Jan 2018 09:26:15 +0100 Subject: [PATCH 40/42] CURA-4525 created CuraSceneController and took out logic from ObjectsModel and BuildPlateModel --- cura/BuildPlateModel.py | 52 ++++------------- cura/CuraApplication.py | 27 ++++++--- cura/CuraSceneController.py | 101 ++++++++++++++++++++++++++++++++++ cura/ObjectManager.py | 87 ----------------------------- cura/ObjectsModel.py | 49 +++++++++++++++++ resources/qml/ObjectsList.qml | 16 +++--- 6 files changed, 187 insertions(+), 145 deletions(-) create mode 100644 cura/CuraSceneController.py delete mode 100644 cura/ObjectManager.py create mode 100644 cura/ObjectsModel.py diff --git a/cura/BuildPlateModel.py b/cura/BuildPlateModel.py index 2d558a91b9..73f61202c6 100644 --- a/cura/BuildPlateModel.py +++ b/cura/BuildPlateModel.py @@ -1,7 +1,6 @@ -from UM.Qt.ListModel import ListModel from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot -from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator -from UM.Scene.SceneNode import SceneNode + +from UM.Qt.ListModel import ListModel from UM.Scene.Selection import Selection from UM.Logger import Logger from UM.Application import Application @@ -14,7 +13,6 @@ class BuildPlateModel(ListModel): def __init__(self): super().__init__() - Application.getInstance().getController().getScene().sceneChanged.connect(self.updateMaxBuildPlate) # it may be a bit inefficient when changing a lot simultaneously Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbers) Selection.selectionChanged.connect(self._updateSelectedObjectBuildPlateNumbers) @@ -22,50 +20,22 @@ class BuildPlateModel(ListModel): self._active_build_plate = -1 self._selection_build_plates = [] - @pyqtSlot(int) - def setActiveBuildPlate(self, nr): - if nr == self._active_build_plate: - return - Logger.log("d", "Select build plate: %s" % nr) - self._active_build_plate = nr - Selection.clear() - - self.activeBuildPlateChanged.emit() - - @pyqtProperty(int, notify = activeBuildPlateChanged) - def activeBuildPlate(self): - return self._active_build_plate + def setMaxBuildPlate(self, max_build_plate): + self._max_build_plate = max_build_plate + self.maxBuildPlateChanged.emit() ## Return the highest build plate number @pyqtProperty(int, notify = maxBuildPlateChanged) def maxBuildPlate(self): return self._max_build_plate - def updateMaxBuildPlate(self, *args): - if args: - source = args[0] - else: - source = None - if not issubclass(type(source), SceneNode): - return - max_build_plate = self._calcMaxBuildPlate() - changed = False - if max_build_plate != self._max_build_plate: - self._max_build_plate = max_build_plate - changed = True - if changed: - self.maxBuildPlateChanged.emit() - build_plates = [{"name": "Build Plate %d" % (i + 1), "buildPlateNumber": i} for i in range(self._max_build_plate + 1)] - self.setItems(build_plates) - self.itemsChanged.emit() + def setActiveBuildPlate(self, nr): + self._active_build_plate = nr + self.activeBuildPlateChanged.emit() - def _calcMaxBuildPlate(self): - max_build_plate = 0 - for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): - if node.callDecoration("isSliceable"): - build_plate_number = node.callDecoration("getBuildPlateNumber") - max_build_plate = max(build_plate_number, max_build_plate) - return max_build_plate + @pyqtProperty(int, notify = activeBuildPlateChanged) + def activeBuildPlate(self): + return self._active_build_plate @staticmethod def createBuildPlateModel(): diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 42bd70fdc8..1e10b6a40c 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -33,7 +33,10 @@ from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.SetTransformOperation import SetTransformOperation from cura.Arranging.Arrange import Arrange +from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob +from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob from cura.Arranging.ShapeArray import ShapeArray +from cura.MultiplyObjectsJob import MultiplyObjectsJob from cura.Scene.ConvexHullDecorator import ConvexHullDecorator from cura.Operations.SetParentOperation import SetParentOperation from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator @@ -41,9 +44,7 @@ from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.Scene.CuraSceneNode import CuraSceneNode -from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob -from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob -from cura.MultiplyObjectsJob import MultiplyObjectsJob +from cura.CuraSceneController import CuraSceneController from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType from UM.Settings.ContainerRegistry import ContainerRegistry @@ -77,7 +78,7 @@ from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisi from cura.Settings.QualitySettingsModel import QualitySettingsModel from cura.Settings.ContainerManager import ContainerManager -from cura.ObjectManager import ObjectManager +from cura.ObjectsModel import ObjectsModel from cura.BuildPlateModel import BuildPlateModel from PyQt5.QtCore import QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS @@ -211,6 +212,7 @@ class CuraApplication(QtApplication): self._build_plate_model = None self._setting_inheritance_manager = None self._simple_mode_settings_manager = None + self._cura_scene_controller = None self._additional_components = {} # Components to add to certain areas in the interface @@ -398,6 +400,8 @@ class CuraApplication(QtApplication): self._plugin_registry.addSupportedPluginExtension("curaplugin", "Cura Plugin") + self.getCuraSceneController().setActiveBuildPlate(0) # Initialize + def _onEngineCreated(self): self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) @@ -699,8 +703,9 @@ class CuraApplication(QtApplication): qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager) - qmlRegisterSingletonType(ObjectManager, "Cura", 1, 2, "ObjectManager", self.getObjectManager) + qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 2, "ObjectsModel", self.getObjectsModel) qmlRegisterSingletonType(BuildPlateModel, "Cura", 1, 2, "BuildPlateModel", self.getBuildPlateModel) + qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 2, "SceneController", self.getCuraSceneController) qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager) @@ -739,18 +744,22 @@ class CuraApplication(QtApplication): self._material_manager = MaterialManager.createMaterialManager() return self._material_manager - def getObjectManager(self, *args): + def getObjectsModel(self, *args): if self._object_manager is None: - self._object_manager = ObjectManager.createObjectManager() + self._object_manager = ObjectsModel.createObjectsModel() return self._object_manager def getBuildPlateModel(self, *args): if self._build_plate_model is None: self._build_plate_model = BuildPlateModel.createBuildPlateModel() - self._build_plate_model.setActiveBuildPlate(0) # default value return self._build_plate_model + def getCuraSceneController(self, *args): + if self._cura_scene_controller is None: + self._cura_scene_controller = CuraSceneController.createCuraSceneController() + return self._cura_scene_controller + def getSettingInheritanceManager(self, *args): if self._setting_inheritance_manager is None: self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager() @@ -1115,7 +1124,7 @@ class CuraApplication(QtApplication): nodes.append(node) job = ArrangeObjectsAllBuildPlatesJob(nodes) job.start() - self.getBuildPlateModel().setActiveBuildPlate(0) + self.getCuraSceneController().setActiveBuildPlate(0) # Initialize # Single build plate @pyqtSlot() diff --git a/cura/CuraSceneController.py b/cura/CuraSceneController.py new file mode 100644 index 0000000000..65723db52c --- /dev/null +++ b/cura/CuraSceneController.py @@ -0,0 +1,101 @@ +from UM.Logger import Logger + +from PyQt5.QtCore import Qt, pyqtSlot, QObject +from PyQt5.QtWidgets import QApplication + +from cura.ObjectsModel import ObjectsModel +from cura.BuildPlateModel import BuildPlateModel + +from UM.Application import Application +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +from UM.Scene.SceneNode import SceneNode +from UM.Scene.Selection import Selection + + +class CuraSceneController(QObject): + def __init__(self, objects_model: ObjectsModel, build_plate_model: BuildPlateModel): + super().__init__() + + self._objects_model = objects_model + self._build_plate_model = build_plate_model + self._active_build_plate = -1 + + self._last_selected_index = 0 + self._max_build_plate = 1 # default + + Application.getInstance().getController().getScene().sceneChanged.connect(self.updateMaxBuildPlate) # it may be a bit inefficient when changing a lot simultaneously + + def updateMaxBuildPlate(self, *args): + if args: + source = args[0] + else: + source = None + if not issubclass(type(source), SceneNode): + return + max_build_plate = self._calcMaxBuildPlate() + changed = False + if max_build_plate != self._max_build_plate: + self._max_build_plate = max_build_plate + changed = True + if changed: + self._build_plate_model.setMaxBuildPlate(self._max_build_plate) + build_plates = [{"name": "Build Plate %d" % (i + 1), "buildPlateNumber": i} for i in range(self._max_build_plate + 1)] + self._build_plate_model.setItems(build_plates) + # self.buildPlateItemsChanged.emit() # TODO: necessary after setItems? + + def _calcMaxBuildPlate(self): + max_build_plate = 0 + for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): + if node.callDecoration("isSliceable"): + build_plate_number = node.callDecoration("getBuildPlateNumber") + max_build_plate = max(build_plate_number, max_build_plate) + return max_build_plate + + ## Either select or deselect an item + @pyqtSlot(int) + def changeSelection(self, index): + modifiers = QApplication.keyboardModifiers() + ctrl_is_active = modifiers & Qt.ControlModifier + shift_is_active = modifiers & Qt.ShiftModifier + + if ctrl_is_active: + item = self._objects_model.getItem(index) + node = item["node"] + if Selection.isSelected(node): + Selection.remove(node) + else: + Selection.add(node) + elif shift_is_active: + polarity = 1 if index + 1 > self._last_selected_index else -1 + for i in range(self._last_selected_index, index + polarity, polarity): + item = self._objects_model.getItem(i) + node = item["node"] + Selection.add(node) + else: + # Single select + item = self._objects_model.getItem(index) + node = item["node"] + Selection.clear() + Selection.add(node) + build_plate_number = node.callDecoration("getBuildPlateNumber") + if build_plate_number is not None and build_plate_number != -1: + self._build_plate_model.setActiveBuildPlate(build_plate_number) + + self._last_selected_index = index + + @pyqtSlot(int) + def setActiveBuildPlate(self, nr): + if nr == self._active_build_plate: + return + Logger.log("d", "Select build plate: %s" % nr) + self._active_build_plate = nr + Selection.clear() + + self._build_plate_model.setActiveBuildPlate(nr) + self._objects_model.setActiveBuildPlate(nr) + + @staticmethod + def createCuraSceneController(): + objects_model = Application.getInstance().getObjectsModel() + build_plate_model = Application.getInstance().getBuildPlateModel() + return CuraSceneController(objects_model = objects_model, build_plate_model = build_plate_model) diff --git a/cura/ObjectManager.py b/cura/ObjectManager.py deleted file mode 100644 index 413f43ed73..0000000000 --- a/cura/ObjectManager.py +++ /dev/null @@ -1,87 +0,0 @@ -from UM.Logger import Logger -from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot -from UM.Application import Application -from UM.Qt.ListModel import ListModel -from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator -from UM.Scene.SceneNode import SceneNode -from UM.Scene.Selection import Selection -from PyQt5.QtCore import Qt -from PyQt5.QtWidgets import QApplication -#from cura.Scene.CuraSceneNode import CuraSceneNode -from UM.Preferences import Preferences - - -## Keep track of all objects in the project -class ObjectManager(ListModel): - def __init__(self): - super().__init__() - self._last_selected_index = 0 - self._build_plate_model = Application.getInstance().getBuildPlateModel() - Application.getInstance().getController().getScene().sceneChanged.connect(self._update) - Preferences.getInstance().preferenceChanged.connect(self._update) - self._build_plate_model.activeBuildPlateChanged.connect(self._update) - - def _update(self, *args): - nodes = [] - filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate") - active_build_plate_number = self._build_plate_model.activeBuildPlate - for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): - if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")): - continue - if not node.callDecoration("isSliceable"): - continue - node_build_plate_number = node.callDecoration("getBuildPlateNumber") - if filter_current_build_plate and node_build_plate_number != active_build_plate_number: - continue - nodes.append({ - "name": node.getName(), - "isSelected": Selection.isSelected(node), - "isOutsideBuildArea": node.isOutsideBuildArea(), - "buildPlateNumber": node_build_plate_number, - "node": node - }) - nodes = sorted(nodes, key=lambda n: n["name"]) - self.setItems(nodes) - - self.itemsChanged.emit() - - ## Either select or deselect an item - @pyqtSlot(int) - def changeSelection(self, index): - modifiers = QApplication.keyboardModifiers() - ctrl_is_active = modifiers & Qt.ControlModifier - shift_is_active = modifiers & Qt.ShiftModifier - - if ctrl_is_active: - item = self.getItem(index) - node = item["node"] - if Selection.isSelected(node): - Selection.remove(node) - else: - Selection.add(node) - elif shift_is_active: - polarity = 1 if index + 1 > self._last_selected_index else -1 - for i in range(self._last_selected_index, index + polarity, polarity): - item = self.getItem(i) - node = item["node"] - Selection.add(node) - else: - # Single select - item = self.getItem(index) - node = item["node"] - Selection.clear() - Selection.add(node) - build_plate_number = node.callDecoration("getBuildPlateNumber") - if build_plate_number is not None and build_plate_number != -1: - self._build_plate_model.setActiveBuildPlate(build_plate_number) - - self._last_selected_index = index - - # testing - for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): - if node.callDecoration("getLayerData"): - Logger.log("d", " ##### NODE: %s", node) - - @staticmethod - def createObjectManager(): - return ObjectManager() diff --git a/cura/ObjectsModel.py b/cura/ObjectsModel.py new file mode 100644 index 0000000000..2e83ee9033 --- /dev/null +++ b/cura/ObjectsModel.py @@ -0,0 +1,49 @@ +from UM.Application import Application +from UM.Qt.ListModel import ListModel +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +from UM.Scene.SceneNode import SceneNode +from UM.Scene.Selection import Selection +from UM.Preferences import Preferences + + +## Keep track of all objects in the project +class ObjectsModel(ListModel): + def __init__(self): + super().__init__() + + Application.getInstance().getController().getScene().sceneChanged.connect(self._update) + Preferences.getInstance().preferenceChanged.connect(self._update) + + self._build_plate_number = -1 + + def setActiveBuildPlate(self, nr): + self._build_plate_number = nr + self._update() + + def _update(self, *args): + nodes = [] + filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate") + active_build_plate_number = self._build_plate_number + for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): + if not issubclass(type(node), SceneNode) or (not node.getMeshData() and not node.callDecoration("getLayerData")): + continue + if not node.callDecoration("isSliceable"): + continue + node_build_plate_number = node.callDecoration("getBuildPlateNumber") + if filter_current_build_plate and node_build_plate_number != active_build_plate_number: + continue + nodes.append({ + "name": node.getName(), + "isSelected": Selection.isSelected(node), + "isOutsideBuildArea": node.isOutsideBuildArea(), + "buildPlateNumber": node_build_plate_number, + "node": node + }) + nodes = sorted(nodes, key=lambda n: n["name"]) + self.setItems(nodes) + + self.itemsChanged.emit() + + @staticmethod + def createObjectsModel(): + return ObjectsModel() diff --git a/resources/qml/ObjectsList.qml b/resources/qml/ObjectsList.qml index a924959581..04afbe2fb0 100644 --- a/resources/qml/ObjectsList.qml +++ b/resources/qml/ObjectsList.qml @@ -85,7 +85,7 @@ Rectangle anchors.fill: parent; onClicked: { - Cura.BuildPlateModel.setActiveBuildPlate(index); + Cura.SceneController.setActiveBuildPlate(index); } } } @@ -131,7 +131,7 @@ Rectangle Rectangle { height: childrenRect.height - color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlight : index % 2 ? palette.base : palette.alternateBase + color: Cura.ObjectsModel.getItem(index).isSelected ? palette.highlight : index % 2 ? palette.base : palette.alternateBase width: parent.width Label { @@ -139,8 +139,8 @@ Rectangle anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("default_margin").width width: parent.width - 2 * UM.Theme.getSize("default_margin").width - 30 - text: Cura.ObjectManager.getItem(index) ? Cura.ObjectManager.getItem(index).name : ""; - color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : (Cura.ObjectManager.getItem(index).isOutsideBuildArea ? palette.mid : palette.text) + text: Cura.ObjectsModel.getItem(index) ? Cura.ObjectsModel.getItem(index).name : ""; + color: Cura.ObjectsModel.getItem(index).isSelected ? palette.highlightedText : (Cura.ObjectsModel.getItem(index).isOutsideBuildArea ? palette.mid : palette.text) elide: Text.ElideRight } @@ -151,8 +151,8 @@ Rectangle anchors.left: nodeNameLabel.right anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.right: parent.right - text: Cura.ObjectManager.getItem(index).buildPlateNumber != -1 ? Cura.ObjectManager.getItem(index).buildPlateNumber + 1 : ""; - color: Cura.ObjectManager.getItem(index).isSelected ? palette.highlightedText : palette.text + text: Cura.ObjectsModel.getItem(index).buildPlateNumber != -1 ? Cura.ObjectsModel.getItem(index).buildPlateNumber + 1 : ""; + color: Cura.ObjectsModel.getItem(index).isSelected ? palette.highlightedText : palette.text elide: Text.ElideRight } @@ -161,7 +161,7 @@ Rectangle anchors.fill: parent; onClicked: { - Cura.ObjectManager.changeSelection(index); + Cura.SceneController.changeSelection(index); } } } @@ -195,7 +195,7 @@ Rectangle ListView { id: listview - model: Cura.ObjectManager + model: Cura.ObjectsModel width: parent.width delegate: objectDelegate } From 840eedbb3dfe46836f8bc2b711be0fe2f17bd046 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 4 Jan 2018 09:35:23 +0100 Subject: [PATCH 41/42] CURA-4525 switch back to build plate 0 when deleting all objects --- cura/CuraApplication.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 1e10b6a40c..bf9e0016cb 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1050,6 +1050,8 @@ class CuraApplication(QtApplication): op.push() Selection.clear() + self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate + ## Reset all translation on nodes with mesh data. @pyqtSlot() def resetAllTranslation(self): @@ -1124,7 +1126,7 @@ class CuraApplication(QtApplication): nodes.append(node) job = ArrangeObjectsAllBuildPlatesJob(nodes) job.start() - self.getCuraSceneController().setActiveBuildPlate(0) # Initialize + self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate # Single build plate @pyqtSlot() From a5d9aac91bee910d4b6ef13c57b5916b723e7bbb Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 4 Jan 2018 10:32:54 +0100 Subject: [PATCH 42/42] CURA-4525 placed view menu items in Build Plate submenu --- resources/qml/Menus/ViewMenu.qml | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/resources/qml/Menus/ViewMenu.qml b/resources/qml/Menus/ViewMenu.qml index 84722e81bf..72bc42bfb0 100644 --- a/resources/qml/Menus/ViewMenu.qml +++ b/resources/qml/Menus/ViewMenu.qml @@ -46,21 +46,26 @@ Menu visible: UM.Preferences.getValue("cura/use_multi_build_plate") } - Instantiator + Menu { - model: Cura.BuildPlateModel - MenuItem { - text: Cura.BuildPlateModel.getItem(index).name; - onTriggered: Cura.BuildPlateModel.setActiveBuildPlate(Cura.BuildPlateModel.getItem(index).buildPlateNumber); - checkable: true; - checked: Cura.BuildPlateModel.getItem(index).buildPlateNumber == Cura.BuildPlateModel.activeBuildPlate; - exclusiveGroup: buildPlateGroup; - visible: UM.Preferences.getValue("cura/use_multi_build_plate") + id: buildPlateMenu; + title: catalog.i18nc("@action:inmenu menubar:view","&Build plate"); + Instantiator + { + model: Cura.BuildPlateModel + MenuItem { + text: Cura.BuildPlateModel.getItem(index).name; + onTriggered: Cura.SceneController.setActiveBuildPlate(Cura.BuildPlateModel.getItem(index).buildPlateNumber); + checkable: true; + checked: Cura.BuildPlateModel.getItem(index).buildPlateNumber == Cura.BuildPlateModel.activeBuildPlate; + exclusiveGroup: buildPlateGroup; + visible: UM.Preferences.getValue("cura/use_multi_build_plate") + } + onObjectAdded: buildPlateMenu.insertItem(index, object); + onObjectRemoved: buildPlateMenu.removeItem(object) } - onObjectAdded: base.insertItem(index, object); - onObjectRemoved: base.removeItem(object) + ExclusiveGroup { id: buildPlateGroup; } } - ExclusiveGroup { id: buildPlateGroup; } MenuSeparator {}