From a11bf31ce6cb549dc987955628c63df9c8425beb Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 29 Mar 2018 13:47:46 +0200 Subject: [PATCH 01/61] Use separate QML files for prepare and monitor stage --- plugins/MonitorStage/MonitorStage.py | 3 +- plugins/PrepareStage/PrepareStage.py | 2 +- .../qml/{Sidebar.qml => MonitorSidebar.qml} | 0 resources/qml/PrepareSidebar.qml | 676 ++++++++++++++++++ 4 files changed, 678 insertions(+), 3 deletions(-) rename resources/qml/{Sidebar.qml => MonitorSidebar.qml} (100%) create mode 100644 resources/qml/PrepareSidebar.qml diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py index 931c205fff..631da71072 100644 --- a/plugins/MonitorStage/MonitorStage.py +++ b/plugins/MonitorStage/MonitorStage.py @@ -73,6 +73,5 @@ class MonitorStage(CuraStage): self.addDisplayComponent("main", main_component_path) def _updateSidebar(self): - # TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor! - sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml") + sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "MonitorSidebar.qml") self.addDisplayComponent("sidebar", sidebar_component_path) diff --git a/plugins/PrepareStage/PrepareStage.py b/plugins/PrepareStage/PrepareStage.py index 9d4d632845..df40476f7f 100644 --- a/plugins/PrepareStage/PrepareStage.py +++ b/plugins/PrepareStage/PrepareStage.py @@ -14,5 +14,5 @@ class PrepareStage(CuraStage): Application.getInstance().engineCreatedSignal.connect(self._engineCreated) def _engineCreated(self): - sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml") + sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "PrepareSidebar.qml") self.addDisplayComponent("sidebar", sidebar_component_path) diff --git a/resources/qml/Sidebar.qml b/resources/qml/MonitorSidebar.qml similarity index 100% rename from resources/qml/Sidebar.qml rename to resources/qml/MonitorSidebar.qml diff --git a/resources/qml/PrepareSidebar.qml b/resources/qml/PrepareSidebar.qml new file mode 100644 index 0000000000..86e866d895 --- /dev/null +++ b/resources/qml/PrepareSidebar.qml @@ -0,0 +1,676 @@ +// Copyright (c) 2017 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.7 +import QtQuick.Controls 2.0 +import QtQuick.Layouts 1.3 + +import UM 1.2 as UM +import Cura 1.0 as Cura +import "Menus" +import "Menus/ConfigurationMenu" + +Rectangle +{ + id: base + + property int currentModeIndex + property bool hideSettings: PrintInformation.preSliced + property bool hideView: Cura.MachineManager.activeMachineName == "" + + // Is there an output device for this printer? + property bool isNetworkPrinter: Cura.MachineManager.activeMachineNetworkKey != "" + property bool printerConnected: Cura.MachineManager.printerConnected + property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands + property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null + + property bool monitoringPrint: UM.Controller.activeStage.stageId == "MonitorStage" + + property variant printDuration: PrintInformation.currentPrintTime + property variant printMaterialLengths: PrintInformation.materialLengths + property variant printMaterialWeights: PrintInformation.materialWeights + property variant printMaterialCosts: PrintInformation.materialCosts + property variant printMaterialNames: PrintInformation.materialNames + + color: UM.Theme.getColor("sidebar") + UM.I18nCatalog { id: catalog; name:"cura"} + + Timer { + id: tooltipDelayTimer + interval: 500 + repeat: false + property var item + property string text + + onTriggered: + { + base.showTooltip(base, {x: 0, y: item.y}, text); + } + } + + function showTooltip(item, position, text) + { + tooltip.text = text; + position = item.mapToItem(base, position.x - UM.Theme.getSize("default_arrow").width, position.y); + tooltip.show(position); + } + + function hideTooltip() + { + tooltip.hide(); + } + + function strPadLeft(string, pad, length) { + return (new Array(length + 1).join(pad) + string).slice(-length); + } + + function getPrettyTime(time) + { + var hours = Math.floor(time / 3600) + time -= hours * 3600 + var minutes = Math.floor(time / 60); + time -= minutes * 60 + var seconds = Math.floor(time); + + var finalTime = strPadLeft(hours, "0", 2) + ':' + strPadLeft(minutes,'0',2)+ ':' + strPadLeft(seconds,'0',2); + return finalTime; + } + + MouseArea + { + anchors.fill: parent + acceptedButtons: Qt.AllButtons + + onWheel: + { + wheel.accepted = true; + } + } + + MachineSelection + { + id: machineSelection + width: base.width - configSelection.width - separator.width + height: UM.Theme.getSize("sidebar_header").height + anchors.top: base.top + anchors.left: parent.left + } + + Rectangle + { + id: separator + visible: configSelection.visible + width: visible ? Math.round(UM.Theme.getSize("sidebar_lining_thin").height / 2) : 0 + height: UM.Theme.getSize("sidebar_header").height + color: UM.Theme.getColor("sidebar_lining_thin") + anchors.left: machineSelection.right + } + + ConfigurationSelection + { + id: configSelection + visible: isNetworkPrinter && printerConnected + width: visible ? Math.round(base.width * 0.15) : 0 + height: UM.Theme.getSize("sidebar_header").height + anchors.top: base.top + anchors.right: parent.right + panelWidth: base.width + } + + SidebarHeader { + id: header + width: parent.width + visible: !hideSettings && (machineExtruderCount.properties.value > 1 || Cura.MachineManager.hasMaterials || Cura.MachineManager.hasVariants) && !monitoringPrint + anchors.top: machineSelection.bottom + + onShowTooltip: base.showTooltip(item, location, text) + onHideTooltip: base.hideTooltip() + } + + Rectangle { + id: headerSeparator + width: parent.width + visible: settingsModeSelection.visible && header.visible + height: visible ? UM.Theme.getSize("sidebar_lining").height : 0 + color: UM.Theme.getColor("sidebar_lining") + anchors.top: header.bottom + anchors.topMargin: visible ? UM.Theme.getSize("sidebar_margin").height : 0 + } + + onCurrentModeIndexChanged: + { + UM.Preferences.setValue("cura/active_mode", currentModeIndex); + if(modesListModel.count > base.currentModeIndex) + { + sidebarContents.replace(modesListModel.get(base.currentModeIndex).item, { "replace": true }) + } + } + + Label + { + id: settingsModeLabel + text: !hideSettings ? catalog.i18nc("@label:listbox", "Print Setup") : catalog.i18nc("@label:listbox", "Print Setup disabled\nG-code files cannot be modified") + renderType: Text.NativeRendering + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width + anchors.top: hideSettings ? machineSelection.bottom : headerSeparator.bottom + anchors.topMargin: UM.Theme.getSize("sidebar_margin").height + width: Math.round(parent.width * 0.45) + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + visible: !monitoringPrint && !hideView + } + + // Settings mode selection toggle + Rectangle + { + id: settingsModeSelection + color: "transparent" + + width: Math.round(parent.width * 0.55) + height: UM.Theme.getSize("sidebar_header_mode_toggle").height + + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width + anchors.topMargin: UM.Theme.getSize("sidebar_margin").height + anchors.top: + { + if (settingsModeLabel.contentWidth >= parent.width - width - UM.Theme.getSize("sidebar_margin").width * 2) + { + return settingsModeLabel.bottom; + } + else + { + return headerSeparator.bottom; + } + } + + visible: !monitoringPrint && !hideSettings && !hideView + + Component + { + id: wizardDelegate + + Button + { + id: control + + height: settingsModeSelection.height + width: Math.round(parent.width / 2) + + anchors.left: parent.left + anchors.leftMargin: model.index * Math.round(settingsModeSelection.width / 2) + anchors.verticalCenter: parent.verticalCenter + + ButtonGroup.group: modeMenuGroup + + checkable: true + checked: base.currentModeIndex == index + onClicked: base.currentModeIndex = index + + onHoveredChanged: + { + if (hovered) + { + tooltipDelayTimer.item = settingsModeSelection + tooltipDelayTimer.text = model.tooltipText + tooltipDelayTimer.start() + } + else + { + tooltipDelayTimer.stop() + base.hideTooltip() + } + } + + background: Rectangle + { + border.width: control.checked ? UM.Theme.getSize("default_lining").width * 2 : UM.Theme.getSize("default_lining").width + border.color: (control.checked || control.pressed) ? UM.Theme.getColor("action_button_active_border") : control.hovered ? UM.Theme.getColor("action_button_hovered_border"): UM.Theme.getColor("action_button_border") + + // for some reason, QtQuick decided to use the color of the background property as text color for the contentItem, so here it is + color: (control.checked || control.pressed) ? UM.Theme.getColor("action_button_active") : control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button") + } + + contentItem: Label + { + text: model.text + font: UM.Theme.getFont("default") + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + renderType: Text.NativeRendering + elide: Text.ElideRight + color: + { + if(control.pressed) + { + return UM.Theme.getColor("action_button_active_text"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered_text"); + } + return UM.Theme.getColor("action_button_text"); + } + } + } + } + + ButtonGroup + { + id: modeMenuGroup + } + + ListView + { + id: modesList + property var index: 0 + model: modesListModel + delegate: wizardDelegate + anchors.top: parent.top + anchors.left: parent.left + width: parent.width + } + } + + StackView + { + id: sidebarContents + + anchors.bottom: footerSeparator.top + anchors.top: settingsModeSelection.bottom + anchors.topMargin: UM.Theme.getSize("sidebar_margin").height + anchors.left: base.left + anchors.right: base.right + visible: !monitoringPrint && !hideSettings + + replaceEnter: Transition { + PropertyAnimation { + property: "opacity" + from: 0 + to:1 + duration: 100 + } + } + + replaceExit: Transition { + PropertyAnimation { + property: "opacity" + from: 1 + to:0 + duration: 100 + } + } + } + + Loader + { + id: controlItem + anchors.bottom: footerSeparator.top + anchors.top: monitoringPrint ? machineSelection.bottom : headerSeparator.bottom + anchors.left: base.left + anchors.right: base.right + sourceComponent: + { + if(monitoringPrint && connectedPrinter != null) + { + if(connectedPrinter.controlItem != null) + { + return connectedPrinter.controlItem + } + } + return null + } + } + + Loader + { + anchors.bottom: footerSeparator.top + anchors.top: monitoringPrint ? machineSelection.bottom : headerSeparator.bottom + anchors.left: base.left + anchors.right: base.right + source: + { + if(controlItem.sourceComponent == null) + { + if(monitoringPrint) + { + return "PrintMonitor.qml" + } else + { + return "SidebarContents.qml" + } + } + else + { + return "" + } + } + } + + Rectangle + { + id: footerSeparator + width: parent.width + height: UM.Theme.getSize("sidebar_lining").height + color: UM.Theme.getColor("sidebar_lining") + anchors.bottom: printSpecs.top + anchors.bottomMargin: Math.round(UM.Theme.getSize("sidebar_margin").height * 2 + UM.Theme.getSize("progressbar").height + UM.Theme.getFont("default_bold").pixelSize) + } + + Item + { + id: printSpecs + anchors.left: parent.left + anchors.bottom: parent.bottom + anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width + anchors.bottomMargin: UM.Theme.getSize("sidebar_margin").height + height: timeDetails.height + costSpec.height + width: base.width - (saveButton.buttonRowWidth + UM.Theme.getSize("sidebar_margin").width) + visible: !monitoringPrint + clip: true + + Label + { + id: timeDetails + anchors.left: parent.left + anchors.bottom: costSpec.top + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text_subtext") + text: (!base.printDuration || !base.printDuration.valid) ? catalog.i18nc("@label Hours and minutes", "00h 00min") : base.printDuration.getDisplayString(UM.DurationFormat.Short) + renderType: Text.NativeRendering + + MouseArea + { + id: timeDetailsMouseArea + anchors.fill: parent + hoverEnabled: true + + onEntered: + { + if(base.printDuration.valid && !base.printDuration.isTotalDurationZero) + { + // All the time information for the different features is achieved + var print_time = PrintInformation.getFeaturePrintTimes(); + var total_seconds = parseInt(base.printDuration.getDisplayString(UM.DurationFormat.Seconds)) + + // A message is created and displayed when the user hover the time label + var tooltip_html = "%1
".arg(catalog.i18nc("@tooltip", "Time specification")); + for(var feature in print_time) + { + if(!print_time[feature].isTotalDurationZero) + { + tooltip_html += "" + + "".arg(print_time[feature].getDisplayString(UM.DurationFormat.ISO8601).slice(0,-3)) + + "".arg(Math.round(100 * parseInt(print_time[feature].getDisplayString(UM.DurationFormat.Seconds)) / total_seconds)) + + ""; + } + } + tooltip_html += "
" + feature + ":  %1  %1%
"; + + base.showTooltip(parent, Qt.point(-UM.Theme.getSize("sidebar_margin").width, 0), tooltip_html); + } + } + onExited: + { + base.hideTooltip(); + } + } + } + + Label + { + function formatRow(items) + { + var row_html = ""; + for(var item = 0; item < items.length; item++) + { + if (item == 0) + { + row_html += "%1".arg(items[item]); + } + else + { + row_html += "  %1".arg(items[item]); + } + } + row_html += ""; + return row_html; + } + + function getSpecsData() + { + var lengths = []; + var total_length = 0; + var weights = []; + var total_weight = 0; + var costs = []; + var total_cost = 0; + var some_costs_known = false; + var names = []; + if(base.printMaterialLengths) + { + for(var index = 0; index < base.printMaterialLengths.length; index++) + { + if(base.printMaterialLengths[index] > 0) + { + names.push(base.printMaterialNames[index]); + lengths.push(base.printMaterialLengths[index].toFixed(2)); + weights.push(String(Math.round(base.printMaterialWeights[index]))); + var cost = base.printMaterialCosts[index] == undefined ? 0 : base.printMaterialCosts[index].toFixed(2); + costs.push(cost); + if(cost > 0) + { + some_costs_known = true; + } + + total_length += base.printMaterialLengths[index]; + total_weight += base.printMaterialWeights[index]; + total_cost += base.printMaterialCosts[index]; + } + } + } + if(lengths.length == 0) + { + lengths = ["0.00"]; + weights = ["0"]; + costs = ["0.00"]; + } + + var tooltip_html = "%1
".arg(catalog.i18nc("@label", "Cost specification")); + for(var index = 0; index < lengths.length; index++) + { + tooltip_html += formatRow([ + "%1:".arg(names[index]), + catalog.i18nc("@label m for meter", "%1m").arg(lengths[index]), + catalog.i18nc("@label g for grams", "%1g").arg(weights[index]), + "%1 %2".arg(UM.Preferences.getValue("cura/currency")).arg(costs[index]), + ]); + } + if(lengths.length > 1) + { + tooltip_html += formatRow([ + catalog.i18nc("@label", "Total:"), + catalog.i18nc("@label m for meter", "%1m").arg(total_length.toFixed(2)), + catalog.i18nc("@label g for grams", "%1g").arg(Math.round(total_weight)), + "%1 %2".arg(UM.Preferences.getValue("cura/currency")).arg(total_cost.toFixed(2)), + ]); + } + tooltip_html += "
"; + tooltipText = tooltip_html; + + return tooltipText + } + + id: costSpec + anchors.left: parent.left + anchors.bottom: parent.bottom + font: UM.Theme.getFont("very_small") + renderType: Text.NativeRendering + color: UM.Theme.getColor("text_subtext") + elide: Text.ElideMiddle + width: parent.width + property string tooltipText + text: + { + var lengths = []; + var weights = []; + var costs = []; + var someCostsKnown = false; + if(base.printMaterialLengths) { + for(var index = 0; index < base.printMaterialLengths.length; index++) + { + if(base.printMaterialLengths[index] > 0) + { + lengths.push(base.printMaterialLengths[index].toFixed(2)); + weights.push(String(Math.round(base.printMaterialWeights[index]))); + var cost = base.printMaterialCosts[index] == undefined ? 0 : base.printMaterialCosts[index].toFixed(2); + costs.push(cost); + if(cost > 0) + { + someCostsKnown = true; + } + } + } + } + if(lengths.length == 0) + { + lengths = ["0.00"]; + weights = ["0"]; + costs = ["0.00"]; + } + var result = lengths.join(" + ") + "m / ~ " + weights.join(" + ") + "g"; + if(someCostsKnown) + { + result += " / ~ " + costs.join(" + ") + " " + UM.Preferences.getValue("cura/currency"); + } + return result; + } + MouseArea + { + id: costSpecMouseArea + anchors.fill: parent + hoverEnabled: true + + onEntered: + { + + if(base.printDuration.valid && !base.printDuration.isTotalDurationZero) + { + var show_data = costSpec.getSpecsData() + + base.showTooltip(parent, Qt.point(-UM.Theme.getSize("sidebar_margin").width, 0), show_data); + } + } + onExited: + { + base.hideTooltip(); + } + } + } + } + + // SaveButton and MonitorButton are actually the bottom footer panels. + // "!monitoringPrint" currently means "show-settings-mode" + SaveButton + { + id: saveButton + implicitWidth: base.width + anchors.top: footerSeparator.bottom + anchors.topMargin: UM.Theme.getSize("sidebar_margin").height + anchors.bottom: parent.bottom + visible: !monitoringPrint + } + + MonitorButton + { + id: monitorButton + implicitWidth: base.width + anchors.top: footerSeparator.bottom + anchors.topMargin: UM.Theme.getSize("sidebar_margin").height + anchors.bottom: parent.bottom + visible: monitoringPrint + } + + SidebarTooltip + { + id: tooltip + } + + // Setting mode: Recommended or Custom + ListModel + { + id: modesListModel + } + + SidebarSimple + { + id: sidebarSimple + visible: false + + onShowTooltip: base.showTooltip(item, location, text) + onHideTooltip: base.hideTooltip() + } + + SidebarAdvanced + { + id: sidebarAdvanced + visible: false + + onShowTooltip: base.showTooltip(item, location, text) + onHideTooltip: base.hideTooltip() + } + + Component.onCompleted: + { + modesListModel.append({ + text: catalog.i18nc("@title:tab", "Recommended"), + tooltipText: catalog.i18nc("@tooltip", "Recommended Print Setup

Print with the recommended settings for the selected printer, material and quality."), + item: sidebarSimple + }) + modesListModel.append({ + text: catalog.i18nc("@title:tab", "Custom"), + tooltipText: catalog.i18nc("@tooltip", "Custom Print Setup

Print with finegrained control over every last bit of the slicing process."), + item: sidebarAdvanced + }) + sidebarContents.replace(modesListModel.get(base.currentModeIndex).item, { "immediate": true }) + + var index = Math.round(UM.Preferences.getValue("cura/active_mode")) + if(index) + { + currentModeIndex = index; + } + } + + UM.SettingPropertyProvider + { + id: machineExtruderCount + + containerStackId: Cura.MachineManager.activeMachineId + key: "machine_extruder_count" + watchedProperties: [ "value" ] + storeIndex: 0 + } + + UM.SettingPropertyProvider + { + id: machineHeatedBed + + containerStackId: Cura.MachineManager.activeMachineId + key: "machine_heated_bed" + watchedProperties: [ "value" ] + storeIndex: 0 + } + + // Make the ConfigurationSelector react when the global container changes, otherwise if Cura is not connected to the printer, + // switching printers make no reaction + Connections + { + target: Cura.MachineManager + onGlobalContainerChanged: + { + base.isNetworkPrinter = Cura.MachineManager.activeMachineNetworkKey != "" + base.printerConnected = Cura.MachineManager.printerOutputDevices.length != 0 + } + } +} From b5de9c750140996210610c506c7d34dc01553dc3 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 29 Mar 2018 14:35:32 +0200 Subject: [PATCH 02/61] Remove print monitor from Prepare sidebar --- resources/qml/PrepareSidebar.qml | 65 ++++---------------------------- 1 file changed, 7 insertions(+), 58 deletions(-) diff --git a/resources/qml/PrepareSidebar.qml b/resources/qml/PrepareSidebar.qml index 86e866d895..f35415ad6f 100644 --- a/resources/qml/PrepareSidebar.qml +++ b/resources/qml/PrepareSidebar.qml @@ -24,8 +24,6 @@ Rectangle property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null - property bool monitoringPrint: UM.Controller.activeStage.stageId == "MonitorStage" - property variant printDuration: PrintInformation.currentPrintTime property variant printMaterialLengths: PrintInformation.materialLengths property variant printMaterialWeights: PrintInformation.materialWeights @@ -120,7 +118,7 @@ Rectangle SidebarHeader { id: header width: parent.width - visible: !hideSettings && (machineExtruderCount.properties.value > 1 || Cura.MachineManager.hasMaterials || Cura.MachineManager.hasVariants) && !monitoringPrint + visible: !hideSettings && (machineExtruderCount.properties.value > 1 || Cura.MachineManager.hasMaterials || Cura.MachineManager.hasVariants) anchors.top: machineSelection.bottom onShowTooltip: base.showTooltip(item, location, text) @@ -158,7 +156,7 @@ Rectangle width: Math.round(parent.width * 0.45) font: UM.Theme.getFont("large") color: UM.Theme.getColor("text") - visible: !monitoringPrint && !hideView + visible: !hideView } // Settings mode selection toggle @@ -185,7 +183,7 @@ Rectangle } } - visible: !monitoringPrint && !hideSettings && !hideView + visible: !hideSettings && !hideView Component { @@ -282,7 +280,7 @@ Rectangle anchors.topMargin: UM.Theme.getSize("sidebar_margin").height anchors.left: base.left anchors.right: base.right - visible: !monitoringPrint && !hideSettings + visible: !hideSettings replaceEnter: Transition { PropertyAnimation { @@ -305,47 +303,11 @@ Rectangle Loader { - id: controlItem anchors.bottom: footerSeparator.top - anchors.top: monitoringPrint ? machineSelection.bottom : headerSeparator.bottom + anchors.top: headerSeparator.bottom anchors.left: base.left anchors.right: base.right - sourceComponent: - { - if(monitoringPrint && connectedPrinter != null) - { - if(connectedPrinter.controlItem != null) - { - return connectedPrinter.controlItem - } - } - return null - } - } - - Loader - { - anchors.bottom: footerSeparator.top - anchors.top: monitoringPrint ? machineSelection.bottom : headerSeparator.bottom - anchors.left: base.left - anchors.right: base.right - source: - { - if(controlItem.sourceComponent == null) - { - if(monitoringPrint) - { - return "PrintMonitor.qml" - } else - { - return "SidebarContents.qml" - } - } - else - { - return "" - } - } + source: "SidebarContents.qml" } Rectangle @@ -367,7 +329,6 @@ Rectangle anchors.bottomMargin: UM.Theme.getSize("sidebar_margin").height height: timeDetails.height + costSpec.height width: base.width - (saveButton.buttonRowWidth + UM.Theme.getSize("sidebar_margin").width) - visible: !monitoringPrint clip: true Label @@ -570,8 +531,7 @@ Rectangle } } - // SaveButton and MonitorButton are actually the bottom footer panels. - // "!monitoringPrint" currently means "show-settings-mode" + // SaveButton is actually the bottom footer panel. SaveButton { id: saveButton @@ -579,17 +539,6 @@ Rectangle anchors.top: footerSeparator.bottom anchors.topMargin: UM.Theme.getSize("sidebar_margin").height anchors.bottom: parent.bottom - visible: !monitoringPrint - } - - MonitorButton - { - id: monitorButton - implicitWidth: base.width - anchors.top: footerSeparator.bottom - anchors.topMargin: UM.Theme.getSize("sidebar_margin").height - anchors.bottom: parent.bottom - visible: monitoringPrint } SidebarTooltip From 07e1a21aeb5f8ec0e8cb0898cfcd30057bfd5225 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 29 Mar 2018 14:46:11 +0200 Subject: [PATCH 03/61] Remove prepare items from Monitor stage --- resources/qml/MonitorButton.qml | 2 + resources/qml/MonitorSidebar.qml | 477 +------------------------------ 2 files changed, 8 insertions(+), 471 deletions(-) diff --git a/resources/qml/MonitorButton.qml b/resources/qml/MonitorButton.qml index 9888b811e4..0bae22e164 100644 --- a/resources/qml/MonitorButton.qml +++ b/resources/qml/MonitorButton.qml @@ -15,6 +15,8 @@ Item id: base; UM.I18nCatalog { id: catalog; name:"cura"} + height: childrenRect.height + UM.Theme.getSize("sidebar_margin").height + property bool printerConnected: Cura.MachineManager.printerConnected property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands property var activePrinter: printerConnected ? Cura.MachineManager.printerOutputDevices[0].activePrinter : null diff --git a/resources/qml/MonitorSidebar.qml b/resources/qml/MonitorSidebar.qml index 86e866d895..b761b05380 100644 --- a/resources/qml/MonitorSidebar.qml +++ b/resources/qml/MonitorSidebar.qml @@ -24,8 +24,6 @@ Rectangle property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null - property bool monitoringPrint: UM.Controller.activeStage.stageId == "MonitorStage" - property variant printDuration: PrintInformation.currentPrintTime property variant printMaterialLengths: PrintInformation.materialLengths property variant printMaterialWeights: PrintInformation.materialWeights @@ -117,202 +115,16 @@ Rectangle panelWidth: base.width } - SidebarHeader { - id: header - width: parent.width - visible: !hideSettings && (machineExtruderCount.properties.value > 1 || Cura.MachineManager.hasMaterials || Cura.MachineManager.hasVariants) && !monitoringPrint - anchors.top: machineSelection.bottom - - onShowTooltip: base.showTooltip(item, location, text) - onHideTooltip: base.hideTooltip() - } - - Rectangle { - id: headerSeparator - width: parent.width - visible: settingsModeSelection.visible && header.visible - height: visible ? UM.Theme.getSize("sidebar_lining").height : 0 - color: UM.Theme.getColor("sidebar_lining") - anchors.top: header.bottom - anchors.topMargin: visible ? UM.Theme.getSize("sidebar_margin").height : 0 - } - - onCurrentModeIndexChanged: - { - UM.Preferences.setValue("cura/active_mode", currentModeIndex); - if(modesListModel.count > base.currentModeIndex) - { - sidebarContents.replace(modesListModel.get(base.currentModeIndex).item, { "replace": true }) - } - } - - Label - { - id: settingsModeLabel - text: !hideSettings ? catalog.i18nc("@label:listbox", "Print Setup") : catalog.i18nc("@label:listbox", "Print Setup disabled\nG-code files cannot be modified") - renderType: Text.NativeRendering - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width - anchors.top: hideSettings ? machineSelection.bottom : headerSeparator.bottom - anchors.topMargin: UM.Theme.getSize("sidebar_margin").height - width: Math.round(parent.width * 0.45) - font: UM.Theme.getFont("large") - color: UM.Theme.getColor("text") - visible: !monitoringPrint && !hideView - } - - // Settings mode selection toggle - Rectangle - { - id: settingsModeSelection - color: "transparent" - - width: Math.round(parent.width * 0.55) - height: UM.Theme.getSize("sidebar_header_mode_toggle").height - - anchors.right: parent.right - anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width - anchors.topMargin: UM.Theme.getSize("sidebar_margin").height - anchors.top: - { - if (settingsModeLabel.contentWidth >= parent.width - width - UM.Theme.getSize("sidebar_margin").width * 2) - { - return settingsModeLabel.bottom; - } - else - { - return headerSeparator.bottom; - } - } - - visible: !monitoringPrint && !hideSettings && !hideView - - Component - { - id: wizardDelegate - - Button - { - id: control - - height: settingsModeSelection.height - width: Math.round(parent.width / 2) - - anchors.left: parent.left - anchors.leftMargin: model.index * Math.round(settingsModeSelection.width / 2) - anchors.verticalCenter: parent.verticalCenter - - ButtonGroup.group: modeMenuGroup - - checkable: true - checked: base.currentModeIndex == index - onClicked: base.currentModeIndex = index - - onHoveredChanged: - { - if (hovered) - { - tooltipDelayTimer.item = settingsModeSelection - tooltipDelayTimer.text = model.tooltipText - tooltipDelayTimer.start() - } - else - { - tooltipDelayTimer.stop() - base.hideTooltip() - } - } - - background: Rectangle - { - border.width: control.checked ? UM.Theme.getSize("default_lining").width * 2 : UM.Theme.getSize("default_lining").width - border.color: (control.checked || control.pressed) ? UM.Theme.getColor("action_button_active_border") : control.hovered ? UM.Theme.getColor("action_button_hovered_border"): UM.Theme.getColor("action_button_border") - - // for some reason, QtQuick decided to use the color of the background property as text color for the contentItem, so here it is - color: (control.checked || control.pressed) ? UM.Theme.getColor("action_button_active") : control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button") - } - - contentItem: Label - { - text: model.text - font: UM.Theme.getFont("default") - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - renderType: Text.NativeRendering - elide: Text.ElideRight - color: - { - if(control.pressed) - { - return UM.Theme.getColor("action_button_active_text"); - } - else if(control.hovered) - { - return UM.Theme.getColor("action_button_hovered_text"); - } - return UM.Theme.getColor("action_button_text"); - } - } - } - } - - ButtonGroup - { - id: modeMenuGroup - } - - ListView - { - id: modesList - property var index: 0 - model: modesListModel - delegate: wizardDelegate - anchors.top: parent.top - anchors.left: parent.left - width: parent.width - } - } - - StackView - { - id: sidebarContents - - anchors.bottom: footerSeparator.top - anchors.top: settingsModeSelection.bottom - anchors.topMargin: UM.Theme.getSize("sidebar_margin").height - anchors.left: base.left - anchors.right: base.right - visible: !monitoringPrint && !hideSettings - - replaceEnter: Transition { - PropertyAnimation { - property: "opacity" - from: 0 - to:1 - duration: 100 - } - } - - replaceExit: Transition { - PropertyAnimation { - property: "opacity" - from: 1 - to:0 - duration: 100 - } - } - } - Loader { id: controlItem anchors.bottom: footerSeparator.top - anchors.top: monitoringPrint ? machineSelection.bottom : headerSeparator.bottom + anchors.top: machineSelection.bottom anchors.left: base.left anchors.right: base.right sourceComponent: { - if(monitoringPrint && connectedPrinter != null) + if(connectedPrinter != null) { if(connectedPrinter.controlItem != null) { @@ -326,20 +138,14 @@ Rectangle Loader { anchors.bottom: footerSeparator.top - anchors.top: monitoringPrint ? machineSelection.bottom : headerSeparator.bottom + anchors.top: machineSelection.bottom anchors.left: base.left anchors.right: base.right source: { if(controlItem.sourceComponent == null) { - if(monitoringPrint) - { - return "PrintMonitor.qml" - } else - { - return "SidebarContents.qml" - } + return "PrintMonitor.qml" } else { @@ -354,242 +160,16 @@ Rectangle width: parent.width height: UM.Theme.getSize("sidebar_lining").height color: UM.Theme.getColor("sidebar_lining") - anchors.bottom: printSpecs.top - anchors.bottomMargin: Math.round(UM.Theme.getSize("sidebar_margin").height * 2 + UM.Theme.getSize("progressbar").height + UM.Theme.getFont("default_bold").pixelSize) - } - - Item - { - id: printSpecs - anchors.left: parent.left - anchors.bottom: parent.bottom - anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width + anchors.bottom: monitorButton.top anchors.bottomMargin: UM.Theme.getSize("sidebar_margin").height - height: timeDetails.height + costSpec.height - width: base.width - (saveButton.buttonRowWidth + UM.Theme.getSize("sidebar_margin").width) - visible: !monitoringPrint - clip: true - - Label - { - id: timeDetails - anchors.left: parent.left - anchors.bottom: costSpec.top - font: UM.Theme.getFont("large") - color: UM.Theme.getColor("text_subtext") - text: (!base.printDuration || !base.printDuration.valid) ? catalog.i18nc("@label Hours and minutes", "00h 00min") : base.printDuration.getDisplayString(UM.DurationFormat.Short) - renderType: Text.NativeRendering - - MouseArea - { - id: timeDetailsMouseArea - anchors.fill: parent - hoverEnabled: true - - onEntered: - { - if(base.printDuration.valid && !base.printDuration.isTotalDurationZero) - { - // All the time information for the different features is achieved - var print_time = PrintInformation.getFeaturePrintTimes(); - var total_seconds = parseInt(base.printDuration.getDisplayString(UM.DurationFormat.Seconds)) - - // A message is created and displayed when the user hover the time label - var tooltip_html = "%1
".arg(catalog.i18nc("@tooltip", "Time specification")); - for(var feature in print_time) - { - if(!print_time[feature].isTotalDurationZero) - { - tooltip_html += "" + - "".arg(print_time[feature].getDisplayString(UM.DurationFormat.ISO8601).slice(0,-3)) + - "".arg(Math.round(100 * parseInt(print_time[feature].getDisplayString(UM.DurationFormat.Seconds)) / total_seconds)) + - ""; - } - } - tooltip_html += "
" + feature + ":  %1  %1%
"; - - base.showTooltip(parent, Qt.point(-UM.Theme.getSize("sidebar_margin").width, 0), tooltip_html); - } - } - onExited: - { - base.hideTooltip(); - } - } - } - - Label - { - function formatRow(items) - { - var row_html = ""; - for(var item = 0; item < items.length; item++) - { - if (item == 0) - { - row_html += "%1".arg(items[item]); - } - else - { - row_html += "  %1".arg(items[item]); - } - } - row_html += ""; - return row_html; - } - - function getSpecsData() - { - var lengths = []; - var total_length = 0; - var weights = []; - var total_weight = 0; - var costs = []; - var total_cost = 0; - var some_costs_known = false; - var names = []; - if(base.printMaterialLengths) - { - for(var index = 0; index < base.printMaterialLengths.length; index++) - { - if(base.printMaterialLengths[index] > 0) - { - names.push(base.printMaterialNames[index]); - lengths.push(base.printMaterialLengths[index].toFixed(2)); - weights.push(String(Math.round(base.printMaterialWeights[index]))); - var cost = base.printMaterialCosts[index] == undefined ? 0 : base.printMaterialCosts[index].toFixed(2); - costs.push(cost); - if(cost > 0) - { - some_costs_known = true; - } - - total_length += base.printMaterialLengths[index]; - total_weight += base.printMaterialWeights[index]; - total_cost += base.printMaterialCosts[index]; - } - } - } - if(lengths.length == 0) - { - lengths = ["0.00"]; - weights = ["0"]; - costs = ["0.00"]; - } - - var tooltip_html = "%1
".arg(catalog.i18nc("@label", "Cost specification")); - for(var index = 0; index < lengths.length; index++) - { - tooltip_html += formatRow([ - "%1:".arg(names[index]), - catalog.i18nc("@label m for meter", "%1m").arg(lengths[index]), - catalog.i18nc("@label g for grams", "%1g").arg(weights[index]), - "%1 %2".arg(UM.Preferences.getValue("cura/currency")).arg(costs[index]), - ]); - } - if(lengths.length > 1) - { - tooltip_html += formatRow([ - catalog.i18nc("@label", "Total:"), - catalog.i18nc("@label m for meter", "%1m").arg(total_length.toFixed(2)), - catalog.i18nc("@label g for grams", "%1g").arg(Math.round(total_weight)), - "%1 %2".arg(UM.Preferences.getValue("cura/currency")).arg(total_cost.toFixed(2)), - ]); - } - tooltip_html += "
"; - tooltipText = tooltip_html; - - return tooltipText - } - - id: costSpec - anchors.left: parent.left - anchors.bottom: parent.bottom - font: UM.Theme.getFont("very_small") - renderType: Text.NativeRendering - color: UM.Theme.getColor("text_subtext") - elide: Text.ElideMiddle - width: parent.width - property string tooltipText - text: - { - var lengths = []; - var weights = []; - var costs = []; - var someCostsKnown = false; - if(base.printMaterialLengths) { - for(var index = 0; index < base.printMaterialLengths.length; index++) - { - if(base.printMaterialLengths[index] > 0) - { - lengths.push(base.printMaterialLengths[index].toFixed(2)); - weights.push(String(Math.round(base.printMaterialWeights[index]))); - var cost = base.printMaterialCosts[index] == undefined ? 0 : base.printMaterialCosts[index].toFixed(2); - costs.push(cost); - if(cost > 0) - { - someCostsKnown = true; - } - } - } - } - if(lengths.length == 0) - { - lengths = ["0.00"]; - weights = ["0"]; - costs = ["0.00"]; - } - var result = lengths.join(" + ") + "m / ~ " + weights.join(" + ") + "g"; - if(someCostsKnown) - { - result += " / ~ " + costs.join(" + ") + " " + UM.Preferences.getValue("cura/currency"); - } - return result; - } - MouseArea - { - id: costSpecMouseArea - anchors.fill: parent - hoverEnabled: true - - onEntered: - { - - if(base.printDuration.valid && !base.printDuration.isTotalDurationZero) - { - var show_data = costSpec.getSpecsData() - - base.showTooltip(parent, Qt.point(-UM.Theme.getSize("sidebar_margin").width, 0), show_data); - } - } - onExited: - { - base.hideTooltip(); - } - } - } - } - - // SaveButton and MonitorButton are actually the bottom footer panels. - // "!monitoringPrint" currently means "show-settings-mode" - SaveButton - { - id: saveButton - implicitWidth: base.width - anchors.top: footerSeparator.bottom - anchors.topMargin: UM.Theme.getSize("sidebar_margin").height - anchors.bottom: parent.bottom - visible: !monitoringPrint } + // MonitorButton is actually the bottom footer panel. MonitorButton { id: monitorButton implicitWidth: base.width - anchors.top: footerSeparator.bottom - anchors.topMargin: UM.Theme.getSize("sidebar_margin").height anchors.bottom: parent.bottom - visible: monitoringPrint } SidebarTooltip @@ -597,51 +177,6 @@ Rectangle id: tooltip } - // Setting mode: Recommended or Custom - ListModel - { - id: modesListModel - } - - SidebarSimple - { - id: sidebarSimple - visible: false - - onShowTooltip: base.showTooltip(item, location, text) - onHideTooltip: base.hideTooltip() - } - - SidebarAdvanced - { - id: sidebarAdvanced - visible: false - - onShowTooltip: base.showTooltip(item, location, text) - onHideTooltip: base.hideTooltip() - } - - Component.onCompleted: - { - modesListModel.append({ - text: catalog.i18nc("@title:tab", "Recommended"), - tooltipText: catalog.i18nc("@tooltip", "Recommended Print Setup

Print with the recommended settings for the selected printer, material and quality."), - item: sidebarSimple - }) - modesListModel.append({ - text: catalog.i18nc("@title:tab", "Custom"), - tooltipText: catalog.i18nc("@tooltip", "Custom Print Setup

Print with finegrained control over every last bit of the slicing process."), - item: sidebarAdvanced - }) - sidebarContents.replace(modesListModel.get(base.currentModeIndex).item, { "immediate": true }) - - var index = Math.round(UM.Preferences.getValue("cura/active_mode")) - if(index) - { - currentModeIndex = index; - } - } - UM.SettingPropertyProvider { id: machineExtruderCount From 272796fd959ac65dc857fb9de8f3542d43166bbc Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 29 Mar 2018 14:46:33 +0200 Subject: [PATCH 04/61] Prevent QML warning --- resources/qml/SidebarHeader.qml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/resources/qml/SidebarHeader.qml b/resources/qml/SidebarHeader.qml index 7590beddc4..1e0a293155 100644 --- a/resources/qml/SidebarHeader.qml +++ b/resources/qml/SidebarHeader.qml @@ -17,8 +17,17 @@ Column property int currentExtruderIndex: Cura.ExtruderManager.activeExtruderIndex; property bool currentExtruderVisible: extrudersList.visible; property bool printerConnected: Cura.MachineManager.printerConnected - property bool hasManyPrinterTypes: printerConnected ? Cura.MachineManager.printerOutputDevices[0].connectedPrintersTypeCount.length > 1 : false - + property bool hasManyPrinterTypes: + { + if (printerConnected) + { + if (Cura.MachineManager.printerOutputDevices[0].connectedPrintersTypeCount != null) + { + return Cura.MachineManager.printerOutputDevices[0].connectedPrintersTypeCount.length > 1; + } + } + return false; + } spacing: Math.round(UM.Theme.getSize("sidebar_margin").width * 0.9) signal showTooltip(Item item, point location, string text) From c101fe005f1fffe10fefb1d6797555465eb39d4a Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 29 Mar 2018 18:28:15 +0200 Subject: [PATCH 05/61] Fix getting multiple extruder temperatures from USB printer --- plugins/USBPrinting/USBPrinterOutputDevice.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index fbb35a1df0..2f88b217c6 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -299,7 +299,21 @@ class USBPrinterOutputDevice(PrinterOutputDevice): if b"ok T:" in line or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line) # Update all temperature values - for match, extruder in zip(extruder_temperature_matches, self._printers[0].extruders): + matched_extruder_nrs = [] + for match in extruder_temperature_matches: + extruder_nr = 0 + if match[0] != b"": + extruder_nr = int(match[0]) + + if extruder_nr in matched_extruder_nrs: + continue + matched_extruder_nrs.append(extruder_nr) + + if extruder_nr >= len(self._printers[0].extruders) + Logger.log("w", "Printer reports more temperatures than the number of configured extruders") + continue + + extruder = self._printers[0].extruders[extruder_nr] if match[1]: extruder.updateHotendTemperature(float(match[1])) if match[2]: From a14d59490918668cfa085cd5703b90f13fc4a3e0 Mon Sep 17 00:00:00 2001 From: JPFrancoia Date: Tue, 10 Apr 2018 16:09:04 +0100 Subject: [PATCH 06/61] - Solve https://github.com/Ultimaker/Cura/issues/3608. - Now account for raft layers when choosing "Pause at layer no". - Now positions the nozzle at (X, Y) of the next layer when resuming. - Cleaning code --- .../scripts/PauseAtHeight.py | 118 +++++++++++++----- 1 file changed, 87 insertions(+), 31 deletions(-) diff --git a/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py index 805ab0a2c3..66221a49b5 100644 --- a/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py +++ b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py @@ -117,6 +117,28 @@ class PauseAtHeight(Script): } }""" + + def getNextXY(self, layer: str): + + """ + Get the X and Y values for a layer (will be used to get X and Y of + the layer after the pause + """ + + lines = layer.split("\n") + + for line in lines: + + if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None: + + x = self.getValue(line, "X") + y = self.getValue(line, "Y") + + return (x, y) + + return (0, 0) + + def execute(self, data: list): """data is a list. Each index contains a layer""" @@ -138,48 +160,72 @@ class PauseAtHeight(Script): resume_temperature = self.getSettingValueByKey("resume_temperature") # T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value") - # with open("out.txt", "w") as f: - # f.write(T) # use offset to calculate the current height: = - layer_0_z = 0. current_z = 0 got_first_g_cmd_on_layer_0 = False + + nbr_negative_layers = 0 + for index, layer in enumerate(data): lines = layer.split("\n") + + # Scroll each line of instruction for each layer in the G-code for line in lines: + + # Fist positive layer reached if ";LAYER:0" in line: layers_started = True + + # Count nbr of negative layers (raft) + elif ";LAYER:-" in line: + nbr_negative_layers += 1 + if not layers_started: continue + # If a Z instruction is in the line, read the current Z if self.getValue(line, "Z") is not None: current_z = self.getValue(line, "Z") if pause_at == "height": + + # Ignore if the line is not G1 or G0 if self.getValue(line, "G") != 1 and self.getValue(line, "G") != 0: continue + # This block is executed once, the first time there is a G + # command, to get the z offset (z for first positive layer) if not got_first_g_cmd_on_layer_0: layer_0_z = current_z got_first_g_cmd_on_layer_0 = True - x = self.getValue(line, "X", x) - y = self.getValue(line, "Y", y) - current_height = current_z - layer_0_z + if current_height < pause_height: - break #Try the next layer. - else: #Pause at layer. + break # Try the next layer. + + # Pause at layer + else: + if not line.startswith(";LAYER:"): continue current_layer = line[len(";LAYER:"):] try: current_layer = int(current_layer) - except ValueError: #Couldn't cast to int. Something is wrong with this g-code data. + + # Couldn't cast to int. Something is wrong with this + # g-code data + except ValueError: continue - if current_layer < pause_layer: - break #Try the next layer. + if current_layer < pause_layer - nbr_negative_layers: + continue + + # Get X and Y from the next layer (better position for + # the nozzle) + nextLayer = data[index + 1] + x, y = self.getNextXY(nextLayer) prevLayer = data[index - 1] prevLines = prevLayer.split("\n") @@ -197,10 +243,17 @@ class PauseAtHeight(Script): prevLayer = data[index - i] layer = prevLayer + layer + with open("/home/djipey/Desktop/out.txt", "w") as f: + ttt = "{} {}".format(x, y) + f.write(ttt) + # Get extruder's absolute position at the # begining of the first layer redone # see https://github.com/nallath/PostProcessingPlugin/issues/55 if i == redo_layers: + # Get X and Y from the next layer (better position for + # the nozzle) + x, y = self.getNextXY(layer) prevLines = prevLayer.split("\n") for line in prevLines: new_e = self.getValue(line, 'E', current_e) @@ -213,57 +266,60 @@ class PauseAtHeight(Script): prepend_gcode += ";added code by post processing\n" prepend_gcode += ";script: PauseAtHeight.py\n" if pause_at == "height": - prepend_gcode += ";current z: {z}\n".format(z = current_z) - prepend_gcode += ";current height: {height}\n".format(height = current_height) + prepend_gcode += ";current z: {z}\n".format(z=current_z) + prepend_gcode += ";current height: {height}\n".format(height=current_height) else: - prepend_gcode += ";current layer: {layer}\n".format(layer = current_layer) + prepend_gcode += ";current layer: {layer}\n".format(layer=current_layer) # Retraction - prepend_gcode += self.putValue(M = 83) + "\n" + prepend_gcode += self.putValue(M=83) + "\n" if retraction_amount != 0: - prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n" + prepend_gcode += self.putValue(G=1, E=-retraction_amount, F=retraction_speed * 60) + "\n" # Move the head away - prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n" - prepend_gcode += self.putValue(G = 1, X = park_x, Y = park_y, F = 9000) + "\n" + prepend_gcode += self.putValue(G=1, Z=current_z + 1, F=300) + "\n" + + # This line should be ok + prepend_gcode += self.putValue(G=1, X=park_x, Y=park_y, F=9000) + "\n" + if current_z < 15: - prepend_gcode += self.putValue(G = 1, Z = 15, F = 300) + "\n" + prepend_gcode += self.putValue(G=1, Z=15, F=300) + "\n" # Disable the E steppers - prepend_gcode += self.putValue(M = 84, E = 0) + "\n" + prepend_gcode += self.putValue(M=84, E=0) + "\n" # Set extruder standby temperature - prepend_gcode += self.putValue(M = 104, S = standby_temperature) + "; standby temperature\n" + prepend_gcode += self.putValue(M=104, S=standby_temperature) + "; standby temperature\n" # Wait till the user continues printing - prepend_gcode += self.putValue(M = 0) + ";Do the actual pause\n" + prepend_gcode += self.putValue(M=0) + ";Do the actual pause\n" # Set extruder resume temperature - prepend_gcode += self.putValue(M = 109, S = resume_temperature) + "; resume temperature\n" + prepend_gcode += self.putValue(M=109, S=resume_temperature) + "; resume temperature\n" # Push the filament back, if retraction_amount != 0: - prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n" + prepend_gcode += self.putValue(G=1, E=retraction_amount, F=retraction_speed * 60) + "\n" # Optionally extrude material if extrude_amount != 0: - prepend_gcode += self.putValue(G = 1, E = extrude_amount, F = extrude_speed * 60) + "\n" + prepend_gcode += self.putValue(G=1, E=extrude_amount, F=extrude_speed * 60) + "\n" # and retract again, the properly primes the nozzle # when changing filament. if retraction_amount != 0: - prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n" + prepend_gcode += self.putValue(G=1, E=-retraction_amount, F=retraction_speed * 60) + "\n" # Move the head back - prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n" - prepend_gcode += self.putValue(G = 1, X = x, Y = y, F = 9000) + "\n" + prepend_gcode += self.putValue(G=1, Z=current_z + 1, F=300) + "\n" + prepend_gcode += self.putValue(G=1, X=x, Y=y, F=9000) + "\n" if retraction_amount != 0: - prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n" - prepend_gcode += self.putValue(G = 1, F = 9000) + "\n" - prepend_gcode += self.putValue(M = 82) + "\n" + prepend_gcode += self.putValue(G=1, E=retraction_amount, F=retraction_speed * 60) + "\n" + prepend_gcode += self.putValue(G=1, F=9000) + "\n" + prepend_gcode += self.putValue(M=82) + "\n" # reset extrude value to pre pause value - prepend_gcode += self.putValue(G = 92, E = current_e) + "\n" + prepend_gcode += self.putValue(G=92, E=current_e) + "\n" layer = prepend_gcode + layer From 33e35e114e2b158344e4fd6a8c0412f3ff585e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20=C3=96hrn?= Date: Sat, 21 Apr 2018 08:00:00 +0200 Subject: [PATCH 07/61] Only open USB ports if the printer understands gcode. --- plugins/USBPrinting/USBPrinterOutputDeviceManager.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index 58b6106fb0..5e9cd8ad1b 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -65,10 +65,11 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): if container_stack is None: time.sleep(5) continue + port_list = [] # Just an empty list; all USB devices will be removed. if container_stack.getMetaDataEntry("supports_usb_connection"): - port_list = self.getSerialPortList(only_list_usb=True) - else: - port_list = [] # Just use an empty list; all USB devices will be removed. + machine_file_formats = [file_type.strip() for file_type in container_stack.getMetaDataEntry("file_formats").split(";")] + if "text/x-gcode" in machine_file_formats: + port_list = self.getSerialPortList(only_list_usb=True) self._addRemovePorts(port_list) time.sleep(5) From f15680b9ea609b4dd836ca664d119dae7ac173cb Mon Sep 17 00:00:00 2001 From: JPFrancoia Date: Thu, 26 Apr 2018 09:47:49 +0100 Subject: [PATCH 08/61] Cleaning dev instructions. --- plugins/PostProcessingPlugin/scripts/PauseAtHeight.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py index 66221a49b5..4795aa3e89 100644 --- a/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py +++ b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py @@ -243,10 +243,6 @@ class PauseAtHeight(Script): prevLayer = data[index - i] layer = prevLayer + layer - with open("/home/djipey/Desktop/out.txt", "w") as f: - ttt = "{} {}".format(x, y) - f.write(ttt) - # Get extruder's absolute position at the # begining of the first layer redone # see https://github.com/nallath/PostProcessingPlugin/issues/55 From 32e2723c26ecbce61819fc087ade20754c0e5b97 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 7 May 2018 10:55:24 +0200 Subject: [PATCH 09/61] Start class layouts for backups and plugins API --- cura/Api/Backups.py | 27 +++++++++++++++++++++++++++ cura/Api/__init__.py | 15 +++++++++++++++ cura/Backups/Backup.py | 29 +++++++++++++++++++++++++++++ cura/Backups/BackupsManager.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+) create mode 100644 cura/Api/Backups.py create mode 100644 cura/Api/__init__.py create mode 100644 cura/Backups/Backup.py create mode 100644 cura/Backups/BackupsManager.py diff --git a/cura/Api/Backups.py b/cura/Api/Backups.py new file mode 100644 index 0000000000..a05c3c3e64 --- /dev/null +++ b/cura/Api/Backups.py @@ -0,0 +1,27 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from zipfile import ZipFile + +from cura.Backups.BackupsManager import BackupsManager + + +class Backups: + """ + The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it. + + Usage: + cura.Api.backups.createBackup() + cura.Api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) + """ + + manager = BackupsManager() # Re-used instance of the backups manager. + + def createBackup(self) -> ("ZipFile", dict): + """ + Create a new backup using the BackupsManager. + :return: + """ + return self.manager.createBackup() + + def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: + return self.manager.restoreBackup(zip_file, meta_data) diff --git a/cura/Api/__init__.py b/cura/Api/__init__.py new file mode 100644 index 0000000000..675e31cf2b --- /dev/null +++ b/cura/Api/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from cura.Api.Backups import Backups + + +class CuraApi: + """ + The official Cura API that plugins can use to interact with Cura. + Python does not technically prevent talking to other classes as well, + but this API provides a version-safe interface with proper deprecation warning etc. + Usage of any other methods than the ones provided in this API can cause plugins to be unstable. + """ + + # Backups API. + backups = Backups() diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py new file mode 100644 index 0000000000..f16dadbfb9 --- /dev/null +++ b/cura/Backups/Backup.py @@ -0,0 +1,29 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + + +class Backup: + """ + The backup class holds all data about a backup. + It is also responsible for reading and writing the zip file to the user data folder. + """ + + def __init__(self): + self.generated = False # type: bool + self.backup_id = None # type: str + self.target_cura_version = None # type: str + self.zip_file = None + self.meta_data = None # type: dict + + def getZipFile(self): + pass + + def getMetaData(self): + pass + + def create(self): + self.generated = True + pass + + def restore(self): + pass diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py new file mode 100644 index 0000000000..f98e0e4d36 --- /dev/null +++ b/cura/Backups/BackupsManager.py @@ -0,0 +1,28 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from zipfile import ZipFile + + +class BackupsManager: + """ + The BackupsManager is responsible for managing the creating and restoring of backups. + Backups themselves are represented in a different class. + """ + + def __init__(self): + pass + + def createBackup(self) -> ("ZipFile", dict): + """ + Get a backup of the current configuration. + :return: A Tuple containing a ZipFile (the actual backup) and a dict containing some meta data (like version). + """ + pass + + def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: + """ + Restore a backup from a given ZipFile. + :param zip_file: A ZipFile containing the actual backup. + :param meta_data: A dict containing some meta data that is needed to restore the backup correctly. + """ + pass From 64819d517eb3ad980fe17948258c0da42a635903 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 7 May 2018 15:09:08 +0200 Subject: [PATCH 10/61] Start implementing backups functionality --- cura/Api/Backups.py | 13 ++++-- cura/Api/__init__.py | 4 ++ cura/Backups/Backup.py | 83 ++++++++++++++++++++++++++++------ cura/Backups/BackupsManager.py | 33 ++++++++++++-- 4 files changed, 110 insertions(+), 23 deletions(-) diff --git a/cura/Api/Backups.py b/cura/Api/Backups.py index a05c3c3e64..aa4d7f9816 100644 --- a/cura/Api/Backups.py +++ b/cura/Api/Backups.py @@ -10,8 +10,10 @@ class Backups: The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it. Usage: - cura.Api.backups.createBackup() - cura.Api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) + from cura.Api import CuraApi + api = CuraApi() + api.backups.createBackup() + api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) """ manager = BackupsManager() # Re-used instance of the backups manager. @@ -19,9 +21,14 @@ class Backups: def createBackup(self) -> ("ZipFile", dict): """ Create a new backup using the BackupsManager. - :return: + :return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup. """ return self.manager.createBackup() def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: + """ + Restore a backup using the BackupManager. + :param zip_file: A ZIP file containing the actual backup data. + :param meta_data: Some meta data needed for restoring a backup, like the Cura version number. + """ return self.manager.restoreBackup(zip_file, meta_data) diff --git a/cura/Api/__init__.py b/cura/Api/__init__.py index 675e31cf2b..4bc38a8297 100644 --- a/cura/Api/__init__.py +++ b/cura/Api/__init__.py @@ -1,5 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from UM.PluginRegistry import PluginRegistry from cura.Api.Backups import Backups @@ -11,5 +12,8 @@ class CuraApi: Usage of any other methods than the ones provided in this API can cause plugins to be unstable. """ + # For now we use the same API version to be consistent. + VERSION = PluginRegistry.APIVersion + # Backups API. backups = Backups() diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index f16dadbfb9..35ee594ad6 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,5 +1,13 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import os +from datetime import datetime +from typing import Optional +from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile + +from UM.Logger import Logger +from UM.Resources import Resources +from cura.CuraApplication import CuraApplication class Backup: @@ -8,22 +16,67 @@ class Backup: It is also responsible for reading and writing the zip file to the user data folder. """ - def __init__(self): - self.generated = False # type: bool - self.backup_id = None # type: str - self.target_cura_version = None # type: str - self.zip_file = None - self.meta_data = None # type: dict + def __init__(self, zip_file: "ZipFile" = None, meta_data: dict = None): + self.zip_file = zip_file # type: Optional[ZipFile] + self.meta_data = meta_data # type: Optional[dict - def getZipFile(self): - pass + def makeFromCurrent(self) -> (bool, Optional[str]): + """ + Create a backup from the current user config folder. + """ + cura_release = CuraApplication.getInstance().getVersion() + version_data_dir = Resources.getDataStoragePath() + timestamp = datetime.now().isoformat() - def getMetaData(self): - pass + Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) - def create(self): - self.generated = True - pass + # We're using an easy to parse filename for when we're restoring edge cases: + # TIMESTAMP.backup.VERSION.cura.zip + archive = self._makeArchive("{}.backup.{}.cura.zip".format(timestamp, cura_release), version_data_dir) - def restore(self): - pass + self.zip_file = archive + self.meta_data = { + "cura_release": cura_release + } + # TODO: fill meta data with machine/material/etc counts. + + @staticmethod + def _makeArchive(root_path: str, archive_name: str) -> Optional[ZipFile]: + """ + Make a full archive from the given root path with the given name. + :param root_path: The root directory to archive recursively. + :param archive_name: The name of the archive to create. + :return: The archive as ZipFile. + """ + parent_folder = os.path.dirname(root_path) + contents = os.walk(root_path) + try: + archive = ZipFile(archive_name, "w", ZIP_DEFLATED) + for root, folders, files in contents: + for folder_name in folders: + # Add all folders, even empty ones. + absolute_path = os.path.join(root, folder_name) + relative_path = absolute_path.replace(parent_folder + '\\', '') + archive.write(absolute_path, relative_path) + for file_name in files: + # Add all files. + absolute_path = os.path.join(root, file_name) + relative_path = absolute_path.replace(parent_folder + '\\', '') + archive.write(absolute_path, relative_path) + archive.close() + return archive + except (IOError, OSError, BadZipfile) as error: + Logger.log("e", "Could not create archive from user data directory: %s", error) + return None + + def restore(self) -> None: + """ + Restore this backup. + """ + if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): + # We can restore without the minimum required information. + Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") + return + + # global_data_dir = os.path.dirname(version_data_dir) + # TODO: restore logic. diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index f98e0e4d36..e649c7ec1f 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -2,6 +2,9 @@ # Cura is released under the terms of the LGPLv3 or higher. from zipfile import ZipFile +from UM.Logger import Logger +from cura.Backups.Backup import Backup + class BackupsManager: """ @@ -9,15 +12,17 @@ class BackupsManager: Backups themselves are represented in a different class. """ - def __init__(self): - pass - def createBackup(self) -> ("ZipFile", dict): """ Get a backup of the current configuration. :return: A Tuple containing a ZipFile (the actual backup) and a dict containing some meta data (like version). """ - pass + self._disableAutoSave() + backup = Backup() + backup.makeFromCurrent() + self._enableAutoSave() + # We don't return a Backup here because we want plugins only to interact with our API and not full objects. + return backup.zip_file, backup.meta_data def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: """ @@ -25,4 +30,22 @@ class BackupsManager: :param zip_file: A ZipFile containing the actual backup. :param meta_data: A dict containing some meta data that is needed to restore the backup correctly. """ - pass + if not meta_data.get("cura_release", None): + # If there is no "cura_release" specified in the meta data, we don't execute a backup restore. + Logger.log("w", "Tried to restore a backup without specifying a Cura version number.") + return + + # TODO: first make a new backup to prevent data loss when restoring fails. + + self._disableAutoSave() + + backup = Backup(zip_file = zip_file, meta_data = meta_data) + backup.restore() # At this point, Cura will need to restart for the changes to take effect + + def _disableAutoSave(self): + """Here we try to disable the auto-save plugin as it might interfere with restoring a backup.""" + # TODO: Disable auto-save if possible. + + def _enableAutoSave(self): + """Re-enable auto-save after we're done.""" + # TODO: Enable auto-save if possible. From 936de402ec16f1293fbd6865c69210657eb10980 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 7 May 2018 15:40:47 +0200 Subject: [PATCH 11/61] use bytes to pass backup file around, generate in memory, small fixes --- cura/Api/Backups.py | 6 ++---- cura/Backups/Backup.py | 25 ++++++++++++------------- cura/Backups/BackupsManager.py | 6 +++--- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/cura/Api/Backups.py b/cura/Api/Backups.py index aa4d7f9816..663715ec29 100644 --- a/cura/Api/Backups.py +++ b/cura/Api/Backups.py @@ -1,7 +1,5 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from zipfile import ZipFile - from cura.Backups.BackupsManager import BackupsManager @@ -18,14 +16,14 @@ class Backups: manager = BackupsManager() # Re-used instance of the backups manager. - def createBackup(self) -> ("ZipFile", dict): + def createBackup(self) -> (bytes, dict): """ Create a new backup using the BackupsManager. :return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup. """ return self.manager.createBackup() - def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: + def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: """ Restore a backup using the BackupManager. :param zip_file: A ZIP file containing the actual backup data. diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 35ee594ad6..6c20df2b2a 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import io import os -from datetime import datetime from typing import Optional from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile @@ -16,9 +16,9 @@ class Backup: It is also responsible for reading and writing the zip file to the user data folder. """ - def __init__(self, zip_file: "ZipFile" = None, meta_data: dict = None): - self.zip_file = zip_file # type: Optional[ZipFile] - self.meta_data = meta_data # type: Optional[dict + def __init__(self, zip_file: bytes = None, meta_data: dict = None): + self.zip_file = zip_file # type: Optional[bytes] + self.meta_data = meta_data # type: Optional[dict] def makeFromCurrent(self) -> (bool, Optional[str]): """ @@ -26,13 +26,12 @@ class Backup: """ cura_release = CuraApplication.getInstance().getVersion() version_data_dir = Resources.getDataStoragePath() - timestamp = datetime.now().isoformat() Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) # We're using an easy to parse filename for when we're restoring edge cases: # TIMESTAMP.backup.VERSION.cura.zip - archive = self._makeArchive("{}.backup.{}.cura.zip".format(timestamp, cura_release), version_data_dir) + archive = self._makeArchive(version_data_dir) self.zip_file = archive self.meta_data = { @@ -41,30 +40,30 @@ class Backup: # TODO: fill meta data with machine/material/etc counts. @staticmethod - def _makeArchive(root_path: str, archive_name: str) -> Optional[ZipFile]: + def _makeArchive(root_path: str) -> Optional[bytes]: """ Make a full archive from the given root path with the given name. :param root_path: The root directory to archive recursively. - :param archive_name: The name of the archive to create. - :return: The archive as ZipFile. + :return: The archive as bytes. """ parent_folder = os.path.dirname(root_path) contents = os.walk(root_path) try: - archive = ZipFile(archive_name, "w", ZIP_DEFLATED) + buffer = io.BytesIO() + archive = ZipFile(buffer, "w", ZIP_DEFLATED) for root, folders, files in contents: for folder_name in folders: # Add all folders, even empty ones. absolute_path = os.path.join(root, folder_name) relative_path = absolute_path.replace(parent_folder + '\\', '') - archive.write(absolute_path, relative_path) + archive.write(relative_path) for file_name in files: # Add all files. absolute_path = os.path.join(root, file_name) relative_path = absolute_path.replace(parent_folder + '\\', '') - archive.write(absolute_path, relative_path) + archive.write(relative_path) archive.close() - return archive + return buffer.getvalue() except (IOError, OSError, BadZipfile) as error: Logger.log("e", "Could not create archive from user data directory: %s", error) return None diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index e649c7ec1f..e43a279ab6 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -1,6 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from zipfile import ZipFile +from typing import Optional from UM.Logger import Logger from cura.Backups.Backup import Backup @@ -12,7 +12,7 @@ class BackupsManager: Backups themselves are represented in a different class. """ - def createBackup(self) -> ("ZipFile", dict): + def createBackup(self) -> (Optional[bytes], Optional[dict]): """ Get a backup of the current configuration. :return: A Tuple containing a ZipFile (the actual backup) and a dict containing some meta data (like version). @@ -24,7 +24,7 @@ class BackupsManager: # We don't return a Backup here because we want plugins only to interact with our API and not full objects. return backup.zip_file, backup.meta_data - def restoreBackup(self, zip_file: "ZipFile", meta_data: dict) -> None: + def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: """ Restore a backup from a given ZipFile. :param zip_file: A ZipFile containing the actual backup. From ce0c14451f935ce3eb36128ab47a427b52dab08a Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 7 May 2018 17:31:10 +0200 Subject: [PATCH 12/61] Fix spelling in API docs --- cura/Api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Api/__init__.py b/cura/Api/__init__.py index 4bc38a8297..c7cb8b5201 100644 --- a/cura/Api/__init__.py +++ b/cura/Api/__init__.py @@ -8,7 +8,7 @@ class CuraApi: """ The official Cura API that plugins can use to interact with Cura. Python does not technically prevent talking to other classes as well, - but this API provides a version-safe interface with proper deprecation warning etc. + but this API provides a version-safe interface with proper deprecation warnings etc. Usage of any other methods than the ones provided in this API can cause plugins to be unstable. """ From a4882d8f83eb966171892e81af6ac909a4368f9c Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 8 May 2018 11:46:09 +0200 Subject: [PATCH 13/61] Fixes for archiving paths in backup, fake meta data --- cura/Backups/Backup.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 6c20df2b2a..3d3cf0be52 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -35,9 +35,13 @@ class Backup: self.zip_file = archive self.meta_data = { - "cura_release": cura_release + "cura_release": cura_release, + "machine_count": 0, + "material_count": 0, + "profile_count": 0, + "plugin_count": 0 } - # TODO: fill meta data with machine/material/etc counts. + # TODO: fill meta data with real machine/material/etc counts. @staticmethod def _makeArchive(root_path: str) -> Optional[bytes]: @@ -46,7 +50,6 @@ class Backup: :param root_path: The root directory to archive recursively. :return: The archive as bytes. """ - parent_folder = os.path.dirname(root_path) contents = os.walk(root_path) try: buffer = io.BytesIO() @@ -55,13 +58,13 @@ class Backup: for folder_name in folders: # Add all folders, even empty ones. absolute_path = os.path.join(root, folder_name) - relative_path = absolute_path.replace(parent_folder + '\\', '') - archive.write(relative_path) + relative_path = absolute_path[len(root_path) + len(os.sep):] + archive.write(absolute_path, relative_path) for file_name in files: # Add all files. absolute_path = os.path.join(root, file_name) - relative_path = absolute_path.replace(parent_folder + '\\', '') - archive.write(relative_path) + relative_path = absolute_path[len(root_path) + len(os.sep):] + archive.write(absolute_path, relative_path) archive.close() return buffer.getvalue() except (IOError, OSError, BadZipfile) as error: From 1b1d99c4bc9b88a96fb73bf452d2b8489f298082 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 8 May 2018 13:22:17 +0200 Subject: [PATCH 14/61] Ignore cura.log in backups --- cura/Backups/Backup.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 3d3cf0be52..50f4383c31 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -16,6 +16,9 @@ class Backup: It is also responsible for reading and writing the zip file to the user data folder. """ + # These files should be ignored when making a backup. + IGNORED_FILES = {"cura.log"} + def __init__(self, zip_file: bytes = None, meta_data: dict = None): self.zip_file = zip_file # type: Optional[bytes] self.meta_data = meta_data # type: Optional[dict] @@ -43,8 +46,7 @@ class Backup: } # TODO: fill meta data with real machine/material/etc counts. - @staticmethod - def _makeArchive(root_path: str) -> Optional[bytes]: + def _makeArchive(self, root_path: str) -> Optional[bytes]: """ Make a full archive from the given root path with the given name. :param root_path: The root directory to archive recursively. @@ -61,7 +63,9 @@ class Backup: relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) for file_name in files: - # Add all files. + # Add all files except the ignored ones. + if file_name in self.IGNORED_FILES: + return absolute_path = os.path.join(root, file_name) relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) From c827703417ad9d1bc29ddd07e87de0e7ffb0fdde Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 8 May 2018 13:47:02 +0200 Subject: [PATCH 15/61] Fix ignoring files --- cura/Backups/Backup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 50f4383c31..af9083ffd7 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -65,7 +65,7 @@ class Backup: for file_name in files: # Add all files except the ignored ones. if file_name in self.IGNORED_FILES: - return + continue absolute_path = os.path.join(root, file_name) relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) From 703e52c0c7dfa3b23d64e31bbc64227d61c3c2de Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 9 May 2018 11:25:19 +0200 Subject: [PATCH 16/61] Ignore cura.cfg in the backups as it might contain secret data from plugins --- cura/Backups/Backup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index af9083ffd7..2bf945ac64 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -17,7 +17,8 @@ class Backup: """ # These files should be ignored when making a backup. - IGNORED_FILES = {"cura.log"} + # Cura.cfg might contain secret data, so we don't back it up for now. + IGNORED_FILES = {"cura.log", "cura.cfg"} def __init__(self, zip_file: bytes = None, meta_data: dict = None): self.zip_file = zip_file # type: Optional[bytes] From bc424509d9f2a346d0d47c33c203d0fc047e6817 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 9 May 2018 17:58:14 +0200 Subject: [PATCH 17/61] Fix docstring --- cura/Backups/BackupsManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index e43a279ab6..bb52ad57ba 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -27,7 +27,7 @@ class BackupsManager: def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: """ Restore a backup from a given ZipFile. - :param zip_file: A ZipFile containing the actual backup. + :param zip_file: A bytes object containing the actual backup. :param meta_data: A dict containing some meta data that is needed to restore the backup correctly. """ if not meta_data.get("cura_release", None): From 79cebca9f0815c1d58939fe65da6ad99a9442c1e Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 11 May 2018 16:45:32 +0200 Subject: [PATCH 18/61] Rudimentary restore functionality --- cura/Backups/Backup.py | 28 +++++++++++++++++++++++----- cura/Backups/BackupsManager.py | 6 +++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 2bf945ac64..3cdcfa8e23 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -2,6 +2,8 @@ # Cura is released under the terms of the LGPLv3 or higher. import io import os +import shutil + from typing import Optional from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile @@ -17,8 +19,7 @@ class Backup: """ # These files should be ignored when making a backup. - # Cura.cfg might contain secret data, so we don't back it up for now. - IGNORED_FILES = {"cura.log", "cura.cfg"} + IGNORED_FILES = {"cura.log"} def __init__(self, zip_file: bytes = None, meta_data: dict = None): self.zip_file = zip_file # type: Optional[bytes] @@ -76,14 +77,31 @@ class Backup: Logger.log("e", "Could not create archive from user data directory: %s", error) return None - def restore(self) -> None: + def restore(self) -> bool: """ Restore this backup. """ if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): # We can restore without the minimum required information. Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") - return + return False # global_data_dir = os.path.dirname(version_data_dir) - # TODO: restore logic. + # TODO: handle restoring older data version. + + version_data_dir = Resources.getDataStoragePath() + archive = ZipFile(io.BytesIO(self.zip_file), "r") + extracted = self._extractArchive(archive, version_data_dir) + if not extracted: + return False + return True + + @staticmethod + def _extractArchive(archive: "ZipFile", target_path: str) -> bool: + Logger.log("d", "Removing current data in location: %s", target_path) + shutil.rmtree(target_path) + + Logger.log("d", "Extracting backup to location: %s", target_path) + archive.extractall(target_path) + + return True diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index bb52ad57ba..55430126dd 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -4,6 +4,7 @@ from typing import Optional from UM.Logger import Logger from cura.Backups.Backup import Backup +from cura.CuraApplication import CuraApplication class BackupsManager: @@ -40,7 +41,10 @@ class BackupsManager: self._disableAutoSave() backup = Backup(zip_file = zip_file, meta_data = meta_data) - backup.restore() # At this point, Cura will need to restart for the changes to take effect + restored = backup.restore() + if restored: + # At this point, Cura will need to restart for the changes to take effect. + CuraApplication.getInstance().windowClosed() def _disableAutoSave(self): """Here we try to disable the auto-save plugin as it might interfere with restoring a backup.""" From a7342f461992e8c63a35d575786fec5826b64a93 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 11 May 2018 17:13:40 +0200 Subject: [PATCH 19/61] Do not safe data after restoring backup --- cura/Backups/Backup.py | 3 +++ cura/Backups/BackupsManager.py | 3 ++- cura/CuraApplication.py | 6 +++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 3cdcfa8e23..866a6269bb 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -34,6 +34,9 @@ class Backup: Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) + # Ensure all current settings are saved. + CuraApplication.getInstance().saveSettings() + # We're using an easy to parse filename for when we're restoring edge cases: # TIMESTAMP.backup.VERSION.cura.zip archive = self._makeArchive(version_data_dir) diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 55430126dd..1f8c706eee 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -44,7 +44,8 @@ class BackupsManager: restored = backup.restore() if restored: # At this point, Cura will need to restart for the changes to take effect. - CuraApplication.getInstance().windowClosed() + # We don't want to store the data at this point as that would override the just-restored backup. + CuraApplication.getInstance().windowClosed(safe_data=False) def _disableAutoSave(self): """Here we try to disable the auto-save plugin as it might interfere with restoring a backup.""" diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index c8adfbc93b..d26d2ae3f3 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -497,10 +497,10 @@ class CuraApplication(QtApplication): ## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently. # # Note that the AutoSave plugin also calls this method. - def saveSettings(self): - if not self.started: # Do not do saving during application start + def saveSettings(self, safe_data: bool = True): + if not self.started or not safe_data: + # Do not do saving during application start or when data should not be safed on quit. return - ContainerRegistry.getInstance().saveDirtyContainers() def saveStack(self, stack): From 0e0492327cb23fe83eeea846dfda1366f149b189 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 11 May 2018 18:15:50 +0200 Subject: [PATCH 20/61] Fix missing argument in application stopped signal callback --- plugins/USBPrinting/USBPrinterOutputDeviceManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index 6375c92879..f586f80dd4 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -54,7 +54,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): self._check_updates = True self._update_thread.start() - def stop(self): + def stop(self, store_data: bool = True): self._check_updates = False def _onConnectionStateChanged(self, serial_port): From 4429b5b5c13c7e048a90407fc72581d6c7ef5f52 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Sat, 12 May 2018 23:35:01 +0200 Subject: [PATCH 21/61] Count backup items for meta data, small fixes --- cura/Backups/Backup.py | 57 +++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 866a6269bb..e851d52ccd 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -19,7 +19,7 @@ class Backup: """ # These files should be ignored when making a backup. - IGNORED_FILES = {"cura.log"} + IGNORED_FILES = {"cura.log", "cache"} def __init__(self, zip_file: bytes = None, meta_data: dict = None): self.zip_file = zip_file # type: Optional[bytes] @@ -37,21 +37,28 @@ class Backup: # Ensure all current settings are saved. CuraApplication.getInstance().saveSettings() - # We're using an easy to parse filename for when we're restoring edge cases: - # TIMESTAMP.backup.VERSION.cura.zip - archive = self._makeArchive(version_data_dir) - - self.zip_file = archive + # Create an empty buffer and write the archive to it. + buffer = io.BytesIO() + archive = self._makeArchive(buffer, version_data_dir) + files = archive.namelist() + + # Count the metadata items. We do this in a rather naive way at the moment. + machine_count = len([s for s in files if "machine_instances/" in s]) - 1 + material_count = len([s for s in files if "materials/" in s]) - 1 + profile_count = len([s for s in files if "quality_changes/" in s]) - 1 + plugin_count = len([s for s in files if "plugin.json" in s]) + + # Store the archive and metadata so the BackupManager can fetch them when needed. + self.zip_file = buffer.getvalue() self.meta_data = { "cura_release": cura_release, - "machine_count": 0, - "material_count": 0, - "profile_count": 0, - "plugin_count": 0 + "machine_count": str(machine_count), + "material_count": str(material_count), + "profile_count": str(profile_count), + "plugin_count": str(plugin_count) } - # TODO: fill meta data with real machine/material/etc counts. - def _makeArchive(self, root_path: str) -> Optional[bytes]: + def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]: """ Make a full archive from the given root path with the given name. :param root_path: The root directory to archive recursively. @@ -59,52 +66,56 @@ class Backup: """ contents = os.walk(root_path) try: - buffer = io.BytesIO() archive = ZipFile(buffer, "w", ZIP_DEFLATED) for root, folders, files in contents: for folder_name in folders: - # Add all folders, even empty ones. + if folder_name in self.IGNORED_FILES: + continue absolute_path = os.path.join(root, folder_name) relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) for file_name in files: - # Add all files except the ignored ones. if file_name in self.IGNORED_FILES: continue absolute_path = os.path.join(root, file_name) relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) archive.close() - return buffer.getvalue() + return archive except (IOError, OSError, BadZipfile) as error: Logger.log("e", "Could not create archive from user data directory: %s", error) + # TODO: show message. return None def restore(self) -> bool: """ - Restore this backup. + Restore this backups + :return: A boolean whether we had success or not. """ if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): # We can restore without the minimum required information. Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") + # TODO: show message. return False - # global_data_dir = os.path.dirname(version_data_dir) # TODO: handle restoring older data version. + # global_data_dir = os.path.dirname(version_data_dir) version_data_dir = Resources.getDataStoragePath() archive = ZipFile(io.BytesIO(self.zip_file), "r") extracted = self._extractArchive(archive, version_data_dir) - if not extracted: - return False - return True + return extracted @staticmethod def _extractArchive(archive: "ZipFile", target_path: str) -> bool: + """ + Extract the whole archive to the given target path. + :param archive: The archive as ZipFile. + :param target_path: The target path. + :return: A boolean whether we had success or not. + """ Logger.log("d", "Removing current data in location: %s", target_path) shutil.rmtree(target_path) - Logger.log("d", "Extracting backup to location: %s", target_path) archive.extractall(target_path) - return True From 2742d61f9b401182afd7df2932923d7a753b0091 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Sun, 13 May 2018 01:20:42 +0200 Subject: [PATCH 22/61] Add TODO for later --- cura/Backups/Backup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index e851d52ccd..4c149b757a 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -33,6 +33,8 @@ class Backup: version_data_dir = Resources.getDataStoragePath() Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) + + # TODO: support preferences file in backup under Linux (is in different directory). # Ensure all current settings are saved. CuraApplication.getInstance().saveSettings() @@ -99,6 +101,7 @@ class Backup: return False # TODO: handle restoring older data version. + # TODO: support preferences file in backup under Linux (is in different directory). # global_data_dir = os.path.dirname(version_data_dir) version_data_dir = Resources.getDataStoragePath() From c43007ca8e54c2aaa224fbf67090c344a18cb807 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Sun, 13 May 2018 15:47:35 +0200 Subject: [PATCH 23/61] Update git ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d991fedb73..98eaa6f414 100644 --- a/.gitignore +++ b/.gitignore @@ -40,6 +40,7 @@ plugins/cura-siemensnx-plugin plugins/CuraBlenderPlugin plugins/CuraCloudPlugin plugins/CuraDrivePlugin +plugins/CuraDrive plugins/CuraLiveScriptingPlugin plugins/CuraOpenSCADPlugin plugins/CuraPrintProfileCreator From 30d66fb8de37cf252a5eb8fa13f19621110f93b2 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 14 May 2018 09:47:44 +0200 Subject: [PATCH 24/61] Copy preferences under Linux to add them to backup, notification messages --- cura/Backups/Backup.py | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 4c149b757a..375a1fa691 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -7,7 +7,11 @@ import shutil from typing import Optional from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile +from UM import i18nCatalog from UM.Logger import Logger +from UM.Message import Message +from UM.Platform import Platform +from UM.Preferences import Preferences from UM.Resources import Resources from cura.CuraApplication import CuraApplication @@ -21,6 +25,9 @@ class Backup: # These files should be ignored when making a backup. IGNORED_FILES = {"cura.log", "cache"} + # Re-use translation catalog. + catalog = i18nCatalog("cura") + def __init__(self, zip_file: bytes = None, meta_data: dict = None): self.zip_file = zip_file # type: Optional[bytes] self.meta_data = meta_data # type: Optional[dict] @@ -31,10 +38,15 @@ class Backup: """ cura_release = CuraApplication.getInstance().getVersion() version_data_dir = Resources.getDataStoragePath() + preferences_file_name = CuraApplication.getInstance().getApplicationName() + preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) - - # TODO: support preferences file in backup under Linux (is in different directory). + + # We copy the preferences file to the user data directory in Linux as it's in a different location there. + # When restoring a backup on Linux, we copy it back. + if Platform.isLinux(): + shutil.copyfile(preferences_file, os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))) # Ensure all current settings are saved. CuraApplication.getInstance().saveSettings() @@ -86,9 +98,15 @@ class Backup: return archive except (IOError, OSError, BadZipfile) as error: Logger.log("e", "Could not create archive from user data directory: %s", error) - # TODO: show message. + self._showMessage( + self.catalog.i18nc("@info:backup_failed", + "Could not create archive from user data directory: {}".format(error))) return None + def _showMessage(self, message: str) -> None: + """Show a UI message""" + Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show() + def restore(self) -> bool: """ Restore this backups @@ -97,16 +115,23 @@ class Backup: if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): # We can restore without the minimum required information. Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") - # TODO: show message. + self._showMessage( + self.catalog.i18nc("@info:backup_failed", + "Tried to restore a Cura backup without having proper data or meta data.")) return False # TODO: handle restoring older data version. - # TODO: support preferences file in backup under Linux (is in different directory). - # global_data_dir = os.path.dirname(version_data_dir) version_data_dir = Resources.getDataStoragePath() archive = ZipFile(io.BytesIO(self.zip_file), "r") extracted = self._extractArchive(archive, version_data_dir) + + # Under Linux, preferences are stored elsewhere, so we copy the file to there. + preferences_file_name = CuraApplication.getInstance().getApplicationName() + preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) + if Platform.isLinux(): + shutil.move(os.path.join(version_data_dir, "{}.cfg".format(preferences_file)), preferences_file) + return extracted @staticmethod From 2ed4b1b014c0aa5c0844022e23c74bb7bfcc45a6 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 14 May 2018 10:16:14 +0200 Subject: [PATCH 25/61] Fixes for restoring preferences on Linux --- cura/Backups/Backup.py | 18 +++++++++++------- cura/Backups/BackupsManager.py | 4 +--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 375a1fa691..ffa0edc80f 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -38,15 +38,17 @@ class Backup: """ cura_release = CuraApplication.getInstance().getVersion() version_data_dir = Resources.getDataStoragePath() - preferences_file_name = CuraApplication.getInstance().getApplicationName() - preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) # We copy the preferences file to the user data directory in Linux as it's in a different location there. - # When restoring a backup on Linux, we copy it back. + # When restoring a backup on Linux, we move it back. if Platform.isLinux(): - shutil.copyfile(preferences_file, os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))) + preferences_file_name = CuraApplication.getInstance().getApplicationName() + preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) + backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name)) + Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file) + shutil.copyfile(preferences_file, backup_preferences_file) # Ensure all current settings are saved. CuraApplication.getInstance().saveSettings() @@ -127,10 +129,12 @@ class Backup: extracted = self._extractArchive(archive, version_data_dir) # Under Linux, preferences are stored elsewhere, so we copy the file to there. - preferences_file_name = CuraApplication.getInstance().getApplicationName() - preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) if Platform.isLinux(): - shutil.move(os.path.join(version_data_dir, "{}.cfg".format(preferences_file)), preferences_file) + preferences_file_name = CuraApplication.getInstance().getApplicationName() + preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) + backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name)) + Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file) + shutil.move(backup_preferences_file, preferences_file) return extracted diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 1f8c706eee..04955692ee 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -36,8 +36,6 @@ class BackupsManager: Logger.log("w", "Tried to restore a backup without specifying a Cura version number.") return - # TODO: first make a new backup to prevent data loss when restoring fails. - self._disableAutoSave() backup = Backup(zip_file = zip_file, meta_data = meta_data) @@ -45,7 +43,7 @@ class BackupsManager: if restored: # At this point, Cura will need to restart for the changes to take effect. # We don't want to store the data at this point as that would override the just-restored backup. - CuraApplication.getInstance().windowClosed(safe_data=False) + CuraApplication.getInstance().windowClosed(save_data=False) def _disableAutoSave(self): """Here we try to disable the auto-save plugin as it might interfere with restoring a backup.""" From 9cb396069034fc78f0f0e39d15803fe07eedd9ae Mon Sep 17 00:00:00 2001 From: Maxim Kulkin Date: Sun, 1 Apr 2018 15:47:36 -0700 Subject: [PATCH 26/61] Add travel_avoid_supports setting support --- cura/BuildVolume.py | 2 +- resources/definitions/fdmprinter.def.json | 10 ++++++++++ resources/i18n/fdmprinter.def.json.pot | 12 ++++++++++++ resources/setting_visibility/advanced.cfg | 1 + resources/setting_visibility/expert.cfg | 1 + 5 files changed, 25 insertions(+), 1 deletion(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 98e087707a..d930ddfb36 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -1035,6 +1035,6 @@ class BuildVolume(SceneNode): _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"] _tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"] _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"] - _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"] + _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"] _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used. _limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"] diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 7e726c5c60..d2af4f11c1 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -3324,6 +3324,16 @@ "settable_per_mesh": false, "settable_per_extruder": true }, + "travel_avoid_supports": + { + "label": "Avoid Supports When Traveling", + "description": "The nozzle avoids already printed supports when traveling. This option is only available when combing is enabled.", + "type": "bool", + "default_value": false, + "enabled": "resolveOrValue('retraction_combing') != 'off' and travel_avoid_other_parts", + "settable_per_mesh": false, + "settable_per_extruder": true + }, "travel_avoid_distance": { "label": "Travel Avoid Distance", diff --git a/resources/i18n/fdmprinter.def.json.pot b/resources/i18n/fdmprinter.def.json.pot index d56e6f7012..e05954c32f 100644 --- a/resources/i18n/fdmprinter.def.json.pot +++ b/resources/i18n/fdmprinter.def.json.pot @@ -3106,6 +3106,18 @@ msgid "" "available when combing is enabled." msgstr "" +#: fdmprinter.def.json +msgctxt "travel_avoid_supports label" +msgid "Avoid Supports When Traveling" +msgstr "" + +#: fdmprinter.def.json +msgctxt "travel_avoid_supports description" +msgid "" +"The nozzle avoids already printed supports when traveling. This option is only " +"available when combing is enabled." +msgstr "" + #: fdmprinter.def.json msgctxt "travel_avoid_distance label" msgid "Travel Avoid Distance" diff --git a/resources/setting_visibility/advanced.cfg b/resources/setting_visibility/advanced.cfg index e68ee787f5..4d4129f2cb 100644 --- a/resources/setting_visibility/advanced.cfg +++ b/resources/setting_visibility/advanced.cfg @@ -72,6 +72,7 @@ jerk_enabled [travel] retraction_combing travel_avoid_other_parts +travel_avoid_supports travel_avoid_distance retraction_hop_enabled retraction_hop_only_when_collides diff --git a/resources/setting_visibility/expert.cfg b/resources/setting_visibility/expert.cfg index d6989f8b26..96c1a5e8d9 100644 --- a/resources/setting_visibility/expert.cfg +++ b/resources/setting_visibility/expert.cfg @@ -187,6 +187,7 @@ jerk_skirt_brim retraction_combing travel_retract_before_outer_wall travel_avoid_other_parts +travel_avoid_supports travel_avoid_distance start_layers_at_same_position layer_start_x From 0617a95cd0797e3739b54e0f89e73fc5e132fe93 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 15 May 2018 11:13:27 +0200 Subject: [PATCH 27/61] Add API root as cura version variable --- plugins/Toolbox/src/Toolbox.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 2ba91dcdba..c5b7ababc5 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -24,17 +24,26 @@ from .PackagesModel import PackagesModel i18n_catalog = i18nCatalog("cura") + ## The Toolbox class is responsible of communicating with the server through the API class Toolbox(QObject, Extension): + + DEFAULT_PACKAGES_API_ROOT = "https://api.ultimaker.com" + def __init__(self, parent=None) -> None: super().__init__(parent) self._application = Application.getInstance() self._package_manager = None self._plugin_registry = Application.getInstance().getPluginRegistry() + self._packages_api_root = self._getPackagesApiRoot() self._packages_version = self._getPackagesVersion() self._api_version = 1 - self._api_url = "https://api.ultimaker.com/cura-packages/v{api_version}/cura/v{package_version}".format( api_version = self._api_version, package_version = self._packages_version) + self._api_url = "{api_root}/cura-packages/v{api_version}/cura/v{package_version}".format( + api_root = self._packages_api_root, + api_version = self._api_version, + package_version = self._packages_version + ) # Network: self._get_packages_request = None @@ -152,6 +161,15 @@ class Toolbox(QObject, Extension): def _onAppInitialized(self) -> None: self._package_manager = Application.getInstance().getCuraPackageManager() + # Get the API root for the packages API depending on Cura version settings. + def _getPackagesApiRoot(self) -> str: + if not hasattr(cura, "CuraVersion"): + return self.DEFAULT_PACKAGES_API_ROOT + if not hasattr(cura.CuraVersion, "CuraPackagesApiRoot"): + return self.DEFAULT_PACKAGES_API_ROOT + return cura.CuraVersion.CuraPackagesApiRoot + + # Get the packages version depending on Cura version settings. def _getPackagesVersion(self) -> int: if not hasattr(cura, "CuraVersion"): return self._plugin_registry.APIVersion From abcf7bd18773c4b2f558e6f8920cbcf1e6ac1344 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 15 May 2018 11:25:56 +0200 Subject: [PATCH 28/61] Show package version in installed tab --- plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml index 6004832a57..b585a084b3 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml @@ -90,6 +90,16 @@ Item color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining") linkColor: UM.Theme.getColor("text_link") } + + Label + { + text: model.version + width: parent.width + height: UM.Theme.getSize("toolbox_property_label").height + color: UM.Theme.getColor("text") + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignLeft + } } ToolboxInstalledTileActions { From c9145c666145a9a7915db39a8aa01647ae4ad25c Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 15 May 2018 13:28:33 +0200 Subject: [PATCH 29/61] Small code improvements --- .../Toolbox/resources/qml/ToolboxInstalledTileActions.qml | 6 ++---- plugins/Toolbox/src/Toolbox.py | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml index 5bbed2351c..3a1b9ba3d9 100644 --- a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml +++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml @@ -18,13 +18,11 @@ Column readyLabel: catalog.i18nc("@action:button", "Update") activeLabel: catalog.i18nc("@action:button", "Updating") completeLabel: catalog.i18nc("@action:button", "Updated") - readyAction: function() { + readyAction: { toolbox.activePackage = model toolbox.update(model.id) } - activeAction: function() { - toolbox.cancelDownload() - } + activeAction: toolbox.cancelDownload() // Don't allow installing while another download is running enabled: !(toolbox.isDownloading && toolbox.activePackage != model) opacity: enabled ? 1.0 : 0.5 diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index cb5272446d..fb74fefdfa 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -313,9 +313,9 @@ class Toolbox(QObject, Extension): if remote_package is None: return False - local_version = local_package["package_version"] - remote_version = remote_package["package_version"] - return Version(remote_version) > Version(local_version) + local_version = Version(local_package["package_version"]) + remote_version = Version(remote_package["package_version"]) + return remote_version > local_version @pyqtSlot(str, result = bool) def isInstalled(self, package_id: str) -> bool: From 21889130b45e44b79bfa749b13107eeed673115f Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 16 May 2018 19:25:19 +0200 Subject: [PATCH 30/61] Code cleanup --- cura/Stages/CuraStage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cura/Stages/CuraStage.py b/cura/Stages/CuraStage.py index 8b7822ed7a..b2f6d61799 100644 --- a/cura/Stages/CuraStage.py +++ b/cura/Stages/CuraStage.py @@ -1,9 +1,10 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import pyqtProperty, QUrl, QObject +from PyQt5.QtCore import pyqtProperty, QUrl from UM.Stage import Stage + class CuraStage(Stage): def __init__(self, parent = None): From eb436a8b0e1ee259c5871fef57f6d19f60f811ad Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 17 May 2018 09:13:55 +0200 Subject: [PATCH 31/61] Ignore permission error on Windows when trying to remove log file --- cura/Backups/Backup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index ffa0edc80f..615fb45297 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -147,7 +147,13 @@ class Backup: :return: A boolean whether we had success or not. """ Logger.log("d", "Removing current data in location: %s", target_path) - shutil.rmtree(target_path) + try: + shutil.rmtree(target_path) + except PermissionError as error: + # This happens if a file is already opened by another program, usually only the log file. + # For now we just ignore this as it doesn't harm the restore process. + Logger.log("w", "Permission error while trying to remove tree: %s", error) + pass Logger.log("d", "Extracting backup to location: %s", target_path) archive.extractall(target_path) return True From fb40ea13cc947f9f3d959e1d8c60ab46291eb834 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 17 May 2018 09:25:03 +0200 Subject: [PATCH 32/61] Try onerror --- cura/Backups/Backup.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 615fb45297..aeffcf80eb 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -138,8 +138,7 @@ class Backup: return extracted - @staticmethod - def _extractArchive(archive: "ZipFile", target_path: str) -> bool: + def _extractArchive(self, archive: "ZipFile", target_path: str) -> bool: """ Extract the whole archive to the given target path. :param archive: The archive as ZipFile. @@ -148,7 +147,7 @@ class Backup: """ Logger.log("d", "Removing current data in location: %s", target_path) try: - shutil.rmtree(target_path) + shutil.rmtree(target_path, onerror=self._onRemoveError) except PermissionError as error: # This happens if a file is already opened by another program, usually only the log file. # For now we just ignore this as it doesn't harm the restore process. @@ -157,3 +156,10 @@ class Backup: Logger.log("d", "Extracting backup to location: %s", target_path) archive.extractall(target_path) return True + + @staticmethod + def _onRemoveError(*args): + import stat # needed for file stat + func, path, _ = args # onerror returns a tuple containing function, path and exception info + os.chmod(path, stat.S_IWRITE) + os.remove(path) From f00459e4cc10cfa1b351b2e15cae1f1eec6305bb Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 17 May 2018 09:32:18 +0200 Subject: [PATCH 33/61] revert --- cura/Backups/Backup.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index aeffcf80eb..615fb45297 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -138,7 +138,8 @@ class Backup: return extracted - def _extractArchive(self, archive: "ZipFile", target_path: str) -> bool: + @staticmethod + def _extractArchive(archive: "ZipFile", target_path: str) -> bool: """ Extract the whole archive to the given target path. :param archive: The archive as ZipFile. @@ -147,7 +148,7 @@ class Backup: """ Logger.log("d", "Removing current data in location: %s", target_path) try: - shutil.rmtree(target_path, onerror=self._onRemoveError) + shutil.rmtree(target_path) except PermissionError as error: # This happens if a file is already opened by another program, usually only the log file. # For now we just ignore this as it doesn't harm the restore process. @@ -156,10 +157,3 @@ class Backup: Logger.log("d", "Extracting backup to location: %s", target_path) archive.extractall(target_path) return True - - @staticmethod - def _onRemoveError(*args): - import stat # needed for file stat - func, path, _ = args # onerror returns a tuple containing function, path and exception info - os.chmod(path, stat.S_IWRITE) - os.remove(path) From 8b0346e11be42637965802f09345cea4126b71f7 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 17 May 2018 10:15:11 +0200 Subject: [PATCH 34/61] For now just ignore locked files on windows --- cura/Backups/Backup.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 615fb45297..c2e795c783 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -138,8 +138,7 @@ class Backup: return extracted - @staticmethod - def _extractArchive(archive: "ZipFile", target_path: str) -> bool: + def _extractArchive(self, archive: "ZipFile", target_path: str) -> bool: """ Extract the whole archive to the given target path. :param archive: The archive as ZipFile. @@ -148,7 +147,7 @@ class Backup: """ Logger.log("d", "Removing current data in location: %s", target_path) try: - shutil.rmtree(target_path) + shutil.rmtree(target_path, ignore_errors=True, onerror=self._handleRemovalError) except PermissionError as error: # This happens if a file is already opened by another program, usually only the log file. # For now we just ignore this as it doesn't harm the restore process. @@ -157,3 +156,8 @@ class Backup: Logger.log("d", "Extracting backup to location: %s", target_path) archive.extractall(target_path) return True + + @staticmethod + def _handleRemovalError(*args): + func, path, _ = args + Logger.log("w", "Could not remove path %s when doing recursive delete, ignoring...", path) From 9410f93dbfae21b3739bb085e7ffeb8ae70d26c4 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 17 May 2018 10:16:13 +0200 Subject: [PATCH 35/61] Remove unused import --- cura/Backups/Backup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index c2e795c783..8bd1bdaeaa 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -11,7 +11,6 @@ from UM import i18nCatalog from UM.Logger import Logger from UM.Message import Message from UM.Platform import Platform -from UM.Preferences import Preferences from UM.Resources import Resources from cura.CuraApplication import CuraApplication From aa07de45ed90a739aa784b99cdbe20810fc8be7a Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 17 May 2018 12:13:01 +0200 Subject: [PATCH 36/61] Start fixing restore on Windows --- cura/Backups/Backup.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 8bd1bdaeaa..9cdd1ab117 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,5 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import fnmatch import io import os import shutil @@ -22,7 +23,7 @@ class Backup: """ # These files should be ignored when making a backup. - IGNORED_FILES = {"cura.log", "cache"} + IGNORED_FILES = {"cura.log", "cache", "(?s).qmlc"} # Re-use translation catalog. catalog = i18nCatalog("cura") @@ -84,14 +85,17 @@ class Backup: archive = ZipFile(buffer, "w", ZIP_DEFLATED) for root, folders, files in contents: for folder_name in folders: - if folder_name in self.IGNORED_FILES: - continue + for ignore_rule in self.IGNORED_FILES: + if fnmatch.fnmatch(ignore_rule, folder_name): + continue absolute_path = os.path.join(root, folder_name) relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) for file_name in files: - if file_name in self.IGNORED_FILES: - continue + for ignore_rule in self.IGNORED_FILES: + if fnmatch.fnmatch(ignore_rule, file_name): + print("FNMATCH=====", ignore_rule, file_name) + continue absolute_path = os.path.join(root, file_name) relative_path = absolute_path[len(root_path) + len(os.sep):] archive.write(absolute_path, relative_path) @@ -145,18 +149,7 @@ class Backup: :return: A boolean whether we had success or not. """ Logger.log("d", "Removing current data in location: %s", target_path) - try: - shutil.rmtree(target_path, ignore_errors=True, onerror=self._handleRemovalError) - except PermissionError as error: - # This happens if a file is already opened by another program, usually only the log file. - # For now we just ignore this as it doesn't harm the restore process. - Logger.log("w", "Permission error while trying to remove tree: %s", error) - pass + Resources.factoryReset() Logger.log("d", "Extracting backup to location: %s", target_path) archive.extractall(target_path) return True - - @staticmethod - def _handleRemovalError(*args): - func, path, _ = args - Logger.log("w", "Could not remove path %s when doing recursive delete, ignoring...", path) From 7b3f334678309f0c9db4533f75fc781912fe0c55 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 17 May 2018 15:07:28 +0200 Subject: [PATCH 37/61] Fix ignored files for Windows --- cura/Backups/Backup.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 9cdd1ab117..5beefb0a1f 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -1,8 +1,9 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import fnmatch import io import os +import re + import shutil from typing import Optional @@ -23,7 +24,7 @@ class Backup: """ # These files should be ignored when making a backup. - IGNORED_FILES = {"cura.log", "cache", "(?s).qmlc"} + IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"] # Re-use translation catalog. catalog = i18nCatalog("cura") @@ -80,25 +81,15 @@ class Backup: :param root_path: The root directory to archive recursively. :return: The archive as bytes. """ - contents = os.walk(root_path) + ignore_string = re.compile("|".join(self.IGNORED_FILES)) try: archive = ZipFile(buffer, "w", ZIP_DEFLATED) - for root, folders, files in contents: - for folder_name in folders: - for ignore_rule in self.IGNORED_FILES: - if fnmatch.fnmatch(ignore_rule, folder_name): - continue - absolute_path = os.path.join(root, folder_name) - relative_path = absolute_path[len(root_path) + len(os.sep):] - archive.write(absolute_path, relative_path) - for file_name in files: - for ignore_rule in self.IGNORED_FILES: - if fnmatch.fnmatch(ignore_rule, file_name): - print("FNMATCH=====", ignore_rule, file_name) - continue - absolute_path = os.path.join(root, file_name) - relative_path = absolute_path[len(root_path) + len(os.sep):] - archive.write(absolute_path, relative_path) + for root, folders, files in os.walk(root_path): + for item_name in folders + files: + absolute_path = os.path.join(root, item_name) + if ignore_string.search(absolute_path): + continue + archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):]) archive.close() return archive except (IOError, OSError, BadZipfile) as error: @@ -141,7 +132,8 @@ class Backup: return extracted - def _extractArchive(self, archive: "ZipFile", target_path: str) -> bool: + @staticmethod + def _extractArchive(archive: "ZipFile", target_path: str) -> bool: """ Extract the whole archive to the given target path. :param archive: The archive as ZipFile. From a0d3dae92057d745508ec8d8c9c869f2b7532182 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 18 May 2018 10:07:20 +0200 Subject: [PATCH 38/61] Do not allow restore different version --- cura/Backups/Backup.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 5beefb0a1f..69718dd7c9 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -14,6 +14,7 @@ from UM.Logger import Logger from UM.Message import Message from UM.Platform import Platform from UM.Resources import Resources +from UM.Version import Version from cura.CuraApplication import CuraApplication @@ -116,7 +117,12 @@ class Backup: "Tried to restore a Cura backup without having proper data or meta data.")) return False - # TODO: handle restoring older data version. + current_version = CuraApplication.getInstance().getVersion() + version_to_restore = self.meta_data.get("cura_release", "master") + if current_version != version_to_restore: + # Cannot restore version older or newer than current because settings might have changed. + # Restoring this will cause a lot of issues so we don't allow this for now. + return False version_data_dir = Resources.getDataStoragePath() archive = ZipFile(io.BytesIO(self.zip_file), "r") From a20de3581a1d1855d2e1b9588db1be15dfa85f05 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 18 May 2018 10:08:00 +0200 Subject: [PATCH 39/61] Add info message when failed because of version mismatch --- cura/Backups/Backup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 69718dd7c9..65d8f184ec 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -122,6 +122,9 @@ class Backup: if current_version != version_to_restore: # Cannot restore version older or newer than current because settings might have changed. # Restoring this will cause a lot of issues so we don't allow this for now. + self._showMessage( + self.catalog.i18nc("@info:backup_failed", + "Tried to restore a Cura backup that does not match your current version.")) return False version_data_dir = Resources.getDataStoragePath() From 2fab1aef33cbbaff18244f3c67beee0424140d6e Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 18 May 2018 17:28:54 +0200 Subject: [PATCH 40/61] Rename Api to API --- cura/Api/Backups.py | 32 -------------------------------- cura/Api/__init__.py | 19 ------------------- 2 files changed, 51 deletions(-) delete mode 100644 cura/Api/Backups.py delete mode 100644 cura/Api/__init__.py diff --git a/cura/Api/Backups.py b/cura/Api/Backups.py deleted file mode 100644 index 663715ec29..0000000000 --- a/cura/Api/Backups.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -from cura.Backups.BackupsManager import BackupsManager - - -class Backups: - """ - The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it. - - Usage: - from cura.Api import CuraApi - api = CuraApi() - api.backups.createBackup() - api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) - """ - - manager = BackupsManager() # Re-used instance of the backups manager. - - def createBackup(self) -> (bytes, dict): - """ - Create a new backup using the BackupsManager. - :return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup. - """ - return self.manager.createBackup() - - def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: - """ - Restore a backup using the BackupManager. - :param zip_file: A ZIP file containing the actual backup data. - :param meta_data: Some meta data needed for restoring a backup, like the Cura version number. - """ - return self.manager.restoreBackup(zip_file, meta_data) diff --git a/cura/Api/__init__.py b/cura/Api/__init__.py deleted file mode 100644 index c7cb8b5201..0000000000 --- a/cura/Api/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -from UM.PluginRegistry import PluginRegistry -from cura.Api.Backups import Backups - - -class CuraApi: - """ - The official Cura API that plugins can use to interact with Cura. - Python does not technically prevent talking to other classes as well, - but this API provides a version-safe interface with proper deprecation warnings etc. - Usage of any other methods than the ones provided in this API can cause plugins to be unstable. - """ - - # For now we use the same API version to be consistent. - VERSION = PluginRegistry.APIVersion - - # Backups API. - backups = Backups() From 5b5a8f77b7065caaafbd8c04878138e422a49fc9 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 18 May 2018 17:29:01 +0200 Subject: [PATCH 41/61] Rename Api to API --- cura/API/Backups.py | 32 ++++++++++++++++++++++++++++++++ cura/API/__init__.py | 19 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 cura/API/Backups.py create mode 100644 cura/API/__init__.py diff --git a/cura/API/Backups.py b/cura/API/Backups.py new file mode 100644 index 0000000000..ba416bd870 --- /dev/null +++ b/cura/API/Backups.py @@ -0,0 +1,32 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from cura.Backups.BackupsManager import BackupsManager + + +class Backups: + """ + The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it. + + Usage: + from cura.API import CuraAPI + api = CuraAPI() + api.backups.createBackup() + api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) + """ + + manager = BackupsManager() # Re-used instance of the backups manager. + + def createBackup(self) -> (bytes, dict): + """ + Create a new backup using the BackupsManager. + :return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup. + """ + return self.manager.createBackup() + + def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: + """ + Restore a backup using the BackupManager. + :param zip_file: A ZIP file containing the actual backup data. + :param meta_data: Some meta data needed for restoring a backup, like the Cura version number. + """ + return self.manager.restoreBackup(zip_file, meta_data) diff --git a/cura/API/__init__.py b/cura/API/__init__.py new file mode 100644 index 0000000000..7dd5d8f79e --- /dev/null +++ b/cura/API/__init__.py @@ -0,0 +1,19 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from UM.PluginRegistry import PluginRegistry +from cura.API.Backups import Backups + + +class CuraAPI: + """ + The official Cura API that plugins can use to interact with Cura. + Python does not technically prevent talking to other classes as well, + but this API provides a version-safe interface with proper deprecation warnings etc. + Usage of any other methods than the ones provided in this API can cause plugins to be unstable. + """ + + # For now we use the same API version to be consistent. + VERSION = PluginRegistry.APIVersion + + # Backups API. + backups = Backups() From f5bed242ed2dc96d81e5758aa3e7e87c26820808 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 22 May 2018 17:13:35 +0200 Subject: [PATCH 42/61] CURA-5370 Small refactor for Arranger: make x and y consistent (numpy arrays start with y first in general), faster, cleanup, more unit tests, take actual build plate size in Arranger instances --- cura/Arranging/Arrange.py | 58 +++-- .../ArrangeObjectsAllBuildPlatesJob.py | 19 +- cura/Arranging/ArrangeObjectsJob.py | 15 +- cura/Arranging/ShapeArray.py | 2 +- cura/BuildVolume.py | 19 +- cura/CuraApplication.py | 23 -- cura/MultiplyObjectsJob.py | 17 +- tests/TestArrange.py | 207 +++++++++++++++++- 8 files changed, 287 insertions(+), 73 deletions(-) diff --git a/cura/Arranging/Arrange.py b/cura/Arranging/Arrange.py index a90a97c3c2..f8c6ae8a31 100644 --- a/cura/Arranging/Arrange.py +++ b/cura/Arranging/Arrange.py @@ -18,17 +18,20 @@ LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points # good locations for objects that you try to put on a build place. # Different priority schemes can be defined so it alters the behavior while using # the same logic. +# +# Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance. class Arrange: build_volume = None - def __init__(self, x, y, offset_x, offset_y, scale= 1.0): - self.shape = (y, x) - self._priority = numpy.zeros((x, y), dtype=numpy.int32) - self._priority_unique_values = [] - self._occupied = numpy.zeros((x, y), dtype=numpy.int32) + def __init__(self, x, y, offset_x, offset_y, scale= 0.5): self._scale = scale # convert input coordinates to arrange coordinates - self._offset_x = offset_x - self._offset_y = offset_y + world_x, world_y = int(x * self._scale), int(y * self._scale) + self._shape = (world_y, world_x) + self._priority = numpy.zeros((world_y, world_x), dtype=numpy.int32) # beware: these are indexed (y, x) + self._priority_unique_values = [] + self._occupied = numpy.zeros((world_y, world_x), dtype=numpy.int32) # beware: these are indexed (y, x) + self._offset_x = int(offset_x * self._scale) + self._offset_y = int(offset_y * self._scale) self._last_priority = 0 self._is_empty = True @@ -39,7 +42,7 @@ 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, x = 220, y = 220): + def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250): arranger = Arrange(x, y, x // 2, y // 2, scale = scale) arranger.centerFirst() @@ -61,13 +64,17 @@ class Arrange: # If a build volume was set, add the disallowed areas if Arrange.build_volume: - disallowed_areas = Arrange.build_volume.getDisallowedAreas() + disallowed_areas = Arrange.build_volume.getDisallowedAreasNoBrim() for area in disallowed_areas: points = copy.deepcopy(area._points) shape_arr = ShapeArray.fromPolygon(points, scale = scale) arranger.place(0, 0, shape_arr, update_empty = False) return arranger + ## This resets the optimization for finding location based on size + def resetLastPriority(self): + self._last_priority = 0 + ## Find placement for a node (using offset shape) and place it (using hull shape) # return the nodes that should be placed # \param node @@ -104,7 +111,7 @@ class Arrange: def centerFirst(self): # Square distance: creates a more round shape self._priority = numpy.fromfunction( - lambda i, j: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self.shape, dtype=numpy.int32) + lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32) self._priority_unique_values = numpy.unique(self._priority) self._priority_unique_values.sort() @@ -112,7 +119,7 @@ class Arrange: # This is a strategy for the arranger. def backFirst(self): self._priority = numpy.fromfunction( - lambda i, j: 10 * j + abs(self._offset_x - i), self.shape, dtype=numpy.int32) + lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32) self._priority_unique_values = numpy.unique(self._priority) self._priority_unique_values.sort() @@ -126,9 +133,15 @@ class Arrange: y = int(self._scale * y) offset_x = x + self._offset_x + shape_arr.offset_x offset_y = y + self._offset_y + shape_arr.offset_y + if offset_x < 0 or offset_y < 0: + return None # out of bounds in self._occupied + occupied_x_max = offset_x + shape_arr.arr.shape[1] + occupied_y_max = offset_y + shape_arr.arr.shape[0] + if occupied_x_max > self._occupied.shape[1] + 1 or occupied_y_max > self._occupied.shape[0] + 1: + return None # out of bounds in self._occupied occupied_slice = self._occupied[ - offset_y:offset_y + shape_arr.arr.shape[0], - offset_x:offset_x + shape_arr.arr.shape[1]] + offset_y:occupied_y_max, + offset_x:occupied_x_max] try: if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]): return None @@ -140,7 +153,7 @@ class Arrange: return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)]) ## Find "best" spot for ShapeArray - # Return namedtuple with properties x, y, penalty_points, priority + # Return namedtuple with properties x, y, penalty_points, priority. # \param shape_arr ShapeArray # \param start_prio Start with this priority value (and skip the ones before) # \param step Slicing value, higher = more skips = faster but less accurate @@ -153,12 +166,11 @@ class Arrange: for priority in self._priority_unique_values[start_idx::step]: tryout_idx = numpy.where(self._priority == priority) for idx in range(len(tryout_idx[0])): - x = tryout_idx[0][idx] - y = tryout_idx[1][idx] - projected_x = x - self._offset_x - projected_y = y - self._offset_y + x = tryout_idx[1][idx] + y = tryout_idx[0][idx] + projected_x = int((x - self._offset_x) / self._scale) + projected_y = int((y - self._offset_y) / self._scale) - # array to "world" coordinates penalty_points = self.checkShape(projected_x, projected_y, shape_arr) if penalty_points is not None: return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority) @@ -191,8 +203,12 @@ class Arrange: # 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 + prio_slice[new_occupied] = 999 + + # If you want to see how the rasterized arranger build plate looks like, uncomment this code + # numpy.set_printoptions(linewidth=500, edgeitems=200) + # print(self._occupied.shape) + # print(self._occupied) @property def isEmpty(self): diff --git a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py index 3f23b0dbe7..1918b32907 100644 --- a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py @@ -1,6 +1,7 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from UM.Application import Application from UM.Job import Job from UM.Scene.SceneNode import SceneNode from UM.Math.Vector import Vector @@ -17,6 +18,7 @@ from cura.Arranging.ShapeArray import ShapeArray from typing import List +## Do an arrangements on a bunch of build plates class ArrangeArray: def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]): self._x = x @@ -79,7 +81,11 @@ class ArrangeObjectsAllBuildPlatesJob(Job): nodes_arr.sort(key=lambda item: item[0]) nodes_arr.reverse() - x, y = 200, 200 + global_container_stack = Application.getInstance().getGlobalContainerStack() + machine_width = global_container_stack.getProperty("machine_width", "value") + machine_depth = global_container_stack.getProperty("machine_depth", "value") + + x, y = machine_width, machine_depth arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = []) arrange_array.add() @@ -93,27 +99,18 @@ class ArrangeObjectsAllBuildPlatesJob(Job): 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: # 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) + best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority) x, y = best_spot.x, best_spot.y node.removeDecorator(ZOffsetDecorator) if node.getBoundingBox(): diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 765c3333cb..01a91a3c22 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -1,6 +1,7 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from UM.Application import Application from UM.Job import Job from UM.Scene.SceneNode import SceneNode from UM.Math.Vector import Vector @@ -32,7 +33,11 @@ class ArrangeObjectsJob(Job): progress = 0, title = i18n_catalog.i18nc("@info:title", "Finding Location")) status_message.show() - arranger = Arrange.create(fixed_nodes = self._fixed_nodes) + global_container_stack = Application.getInstance().getGlobalContainerStack() + machine_width = global_container_stack.getProperty("machine_width", "value") + machine_depth = global_container_stack.getProperty("machine_depth", "value") + + arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes) # Collect nodes to be placed nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) @@ -50,15 +55,15 @@ class ArrangeObjectsJob(Job): last_size = None grouped_operation = GroupedOperation() found_solution_for_all = True + not_fit_count = 0 for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr): # For performance reasons, we assume that when a location does not fit, # it will also not fit for the next object (while what can be untrue). - # We also skip possibilities by slicing through the possibilities (step = 10) if last_size == size: # This optimization works if many of the objects have the same size start_priority = last_priority else: start_priority = 0 - best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10) + best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority) x, y = best_spot.x, best_spot.y node.removeDecorator(ZOffsetDecorator) if node.getBoundingBox(): @@ -70,12 +75,12 @@ class ArrangeObjectsJob(Job): last_priority = best_spot.priority arranger.place(x, y, hull_shape_arr) # take place before the next one - grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True)) else: Logger.log("d", "Arrange all: could not find spot!") found_solution_for_all = False - grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, - idx * 20), set_position = True)) + grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, -not_fit_count * 20), set_position = True)) + not_fit_count += 1 status_message.setProgress((idx + 1) / len(nodes_arr) * 100) Job.yieldThread() diff --git a/cura/Arranging/ShapeArray.py b/cura/Arranging/ShapeArray.py index 68be9a6478..ab785cc3e1 100644 --- a/cura/Arranging/ShapeArray.py +++ b/cura/Arranging/ShapeArray.py @@ -74,7 +74,7 @@ class ShapeArray: # \param vertices @classmethod def arrayFromPolygon(cls, shape, vertices): - base_array = numpy.zeros(shape, dtype=float) # Initialize your array of zeros + base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 98e087707a..ea0d8e1565 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -25,6 +25,7 @@ catalog = i18nCatalog("cura") import numpy import math +import copy from typing import List, Optional @@ -61,6 +62,7 @@ class BuildVolume(SceneNode): self._grid_shader = None self._disallowed_areas = [] + self._disallowed_areas_no_brim = [] self._disallowed_area_mesh = None self._error_areas = [] @@ -171,6 +173,9 @@ class BuildVolume(SceneNode): def getDisallowedAreas(self) -> List[Polygon]: return self._disallowed_areas + def getDisallowedAreasNoBrim(self) -> List[Polygon]: + return self._disallowed_areas_no_brim + def setDisallowedAreas(self, areas: List[Polygon]): self._disallowed_areas = areas @@ -658,7 +663,8 @@ class BuildVolume(SceneNode): result_areas = self._computeDisallowedAreasStatic(disallowed_border_size, used_extruders) #Normal machine disallowed areas can always be added. prime_areas = self._computeDisallowedAreasPrimeBlob(disallowed_border_size, used_extruders) - prime_disallowed_areas = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking. + result_areas_no_brim = self._computeDisallowedAreasStatic(0, used_extruders) #Where the priming is not allowed to happen. This is not added to the result, just for collision checking. + prime_disallowed_areas = copy.deepcopy(result_areas_no_brim) #Check if prime positions intersect with disallowed areas. for extruder in used_extruders: @@ -687,12 +693,15 @@ class BuildVolume(SceneNode): break result_areas[extruder_id].extend(prime_areas[extruder_id]) + result_areas_no_brim[extruder_id].extend(prime_areas[extruder_id]) nozzle_disallowed_areas = extruder.getProperty("nozzle_disallowed_areas", "value") for area in nozzle_disallowed_areas: polygon = Polygon(numpy.array(area, numpy.float32)) - polygon = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size)) - result_areas[extruder_id].append(polygon) #Don't perform the offset on these. + polygon_disallowed_border = polygon.getMinkowskiHull(Polygon.approximatedCircle(disallowed_border_size)) + result_areas[extruder_id].append(polygon_disallowed_border) #Don't perform the offset on these. + #polygon_minimal_border = polygon.getMinkowskiHull(5) + result_areas_no_brim[extruder_id].append(polygon) # no brim # Add prime tower location as disallowed area. if len(used_extruders) > 1: #No prime tower in single-extrusion. @@ -708,6 +717,7 @@ class BuildVolume(SceneNode): break if not prime_tower_collision: result_areas[extruder_id].extend(prime_tower_areas[extruder_id]) + result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id]) else: self._error_areas.extend(prime_tower_areas[extruder_id]) @@ -716,6 +726,9 @@ class BuildVolume(SceneNode): self._disallowed_areas = [] for extruder_id in result_areas: self._disallowed_areas.extend(result_areas[extruder_id]) + self._disallowed_areas_no_brim = [] + for extruder_id in result_areas_no_brim: + self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id]) ## Computes the disallowed areas for objects that are printed with print # features. diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 034d045ce6..9c752cb7a2 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1260,29 +1260,6 @@ class CuraApplication(QtApplication): nodes.append(node) self.arrange(nodes, fixed_nodes = []) - ## Arrange Selection - @pyqtSlot() - def arrangeSelection(self): - nodes = Selection.getAllSelectedObjects() - - # What nodes are on the build plate and are not being moved - fixed_nodes = [] - for node in DepthFirstIterator(self.getController().getScene().getRoot()): - if not isinstance(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(): - continue # i.e. node with layer data - if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): - continue # i.e. node with layer data - if node in nodes: # exclude selected node from fixed_nodes - continue - fixed_nodes.append(node) - self.arrange(nodes, fixed_nodes) - ## Arrange a set of nodes given a set of fixed nodes # \param nodes nodes that we have to place # \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py index 3444da249f..46f7f56f8a 100644 --- a/cura/MultiplyObjectsJob.py +++ b/cura/MultiplyObjectsJob.py @@ -30,11 +30,18 @@ class MultiplyObjectsJob(Job): total_progress = len(self._objects) * self._count current_progress = 0 + global_container_stack = Application.getInstance().getGlobalContainerStack() + machine_width = global_container_stack.getProperty("machine_width", "value") + machine_depth = global_container_stack.getProperty("machine_depth", "value") + root = scene.getRoot() - arranger = Arrange.create(scene_root=root) + scale = 0.5 + arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale) processed_nodes = [] nodes = [] + not_fit_count = 0 + for node in self._objects: # If object is part of a group, multiply group current_node = node @@ -46,12 +53,13 @@ class MultiplyObjectsJob(Job): processed_nodes.append(current_node) node_too_big = False - if node.getBoundingBox().width < 300 or node.getBoundingBox().depth < 300: - offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset=self._min_offset) + if node.getBoundingBox().width < machine_width or node.getBoundingBox().depth < machine_depth: + offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(current_node, min_offset = self._min_offset, scale = scale) else: node_too_big = True found_solution_for_all = True + arranger.resetLastPriority() for i in range(self._count): # We do place the nodes one by one, as we want to yield in between. if not node_too_big: @@ -59,8 +67,9 @@ class MultiplyObjectsJob(Job): if node_too_big or not solution_found: found_solution_for_all = False new_location = new_node.getPosition() - new_location = new_location.set(z = 100 - i * 20) + new_location = new_location.set(z = - not_fit_count * 20) new_node.setPosition(new_location) + not_fit_count += 1 # Same build plate build_plate_number = current_node.callDecoration("getBuildPlateNumber") diff --git a/tests/TestArrange.py b/tests/TestArrange.py index 4f6bb64118..354bbf4962 100755 --- a/tests/TestArrange.py +++ b/tests/TestArrange.py @@ -4,9 +4,17 @@ from cura.Arranging.Arrange import Arrange from cura.Arranging.ShapeArray import ShapeArray -def gimmeShapeArray(): - vertices = numpy.array([[-3, 1], [3, 1], [0, -3]]) - shape_arr = ShapeArray.fromPolygon(vertices) +## Triangle of area 12 +def gimmeShapeArray(scale = 1.0): + vertices = numpy.array([[-3, 1], [3, 1], [0, -3]], dtype=numpy.int32) + shape_arr = ShapeArray.fromPolygon(vertices, scale = scale) + return shape_arr + + +## Boring square +def gimmeShapeArraySquare(scale = 1.0): + vertices = numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32) + shape_arr = ShapeArray.fromPolygon(vertices, scale = scale) return shape_arr @@ -20,6 +28,45 @@ def test_smoke_ShapeArray(): shape_arr = gimmeShapeArray() +## Test ShapeArray +def test_ShapeArray(): + scale = 1 + ar = Arrange(16, 16, 8, 8, scale = scale) + ar.centerFirst() + + shape_arr = gimmeShapeArray(scale) + print(shape_arr.arr) + count = len(numpy.where(shape_arr.arr == 1)[0]) + print(count) + assert count >= 10 # should approach 12 + + +## Test ShapeArray with scaling +def test_ShapeArray_scaling(): + scale = 2 + ar = Arrange(16, 16, 8, 8, scale = scale) + ar.centerFirst() + + shape_arr = gimmeShapeArray(scale) + print(shape_arr.arr) + count = len(numpy.where(shape_arr.arr == 1)[0]) + print(count) + assert count >= 40 # should approach 2*2*12 = 48 + + +## Test ShapeArray with scaling +def test_ShapeArray_scaling2(): + scale = 0.5 + ar = Arrange(16, 16, 8, 8, scale = scale) + ar.centerFirst() + + shape_arr = gimmeShapeArray(scale) + print(shape_arr.arr) + count = len(numpy.where(shape_arr.arr == 1)[0]) + print(count) + assert count >= 1 # should approach 3, but it can be inaccurate due to pixel rounding + + ## Test centerFirst def test_centerFirst(): ar = Arrange(300, 300, 150, 150) @@ -32,13 +79,33 @@ def test_centerFirst(): assert ar._priority[150][150] < ar._priority[130][130] +## Test centerFirst +def test_centerFirst_rectangular(): + ar = Arrange(400, 300, 200, 150) + ar.centerFirst() + assert ar._priority[150][200] < ar._priority[150][220] + assert ar._priority[150][200] < ar._priority[170][200] + assert ar._priority[150][200] < ar._priority[170][220] + assert ar._priority[150][200] < ar._priority[180][150] + assert ar._priority[150][200] < ar._priority[130][200] + assert ar._priority[150][200] < ar._priority[130][180] + + +## Test centerFirst +def test_centerFirst_rectangular(): + ar = Arrange(10, 20, 5, 10) + ar.centerFirst() + print(ar._priority) + assert ar._priority[10][5] < ar._priority[10][7] + + ## Test backFirst def test_backFirst(): ar = Arrange(300, 300, 150, 150) ar.backFirst() - assert ar._priority[150][150] < ar._priority[150][170] + assert ar._priority[150][150] < ar._priority[170][150] assert ar._priority[150][150] < ar._priority[170][170] - assert ar._priority[150][150] > ar._priority[150][130] + assert ar._priority[150][150] > ar._priority[130][150] assert ar._priority[150][150] > ar._priority[130][130] @@ -55,6 +122,113 @@ def test_smoke_bestSpot(): assert hasattr(best_spot, "priority") +## Real life test +def test_bestSpot(): + ar = Arrange(16, 16, 8, 8) + ar.centerFirst() + + shape_arr = gimmeShapeArray() + best_spot = ar.bestSpot(shape_arr) + assert best_spot.x == 0 + assert best_spot.y == 0 + ar.place(best_spot.x, best_spot.y, shape_arr) + + # Place object a second time + best_spot = ar.bestSpot(shape_arr) + assert best_spot.x is not None # we found a location + assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location + ar.place(best_spot.x, best_spot.y, shape_arr) + + print(ar._occupied) # For debugging + + +## Real life test rectangular build plate +def test_bestSpot_rectangular_build_plate(): + ar = Arrange(16, 40, 8, 20) + ar.centerFirst() + + shape_arr = gimmeShapeArray() + best_spot = ar.bestSpot(shape_arr) + ar.place(best_spot.x, best_spot.y, shape_arr) + assert best_spot.x == 0 + assert best_spot.y == 0 + + # Place object a second time + best_spot2 = ar.bestSpot(shape_arr) + assert best_spot2.x is not None # we found a location + assert best_spot2.x != 0 or best_spot2.y != 0 # it can't be on the same location + ar.place(best_spot2.x, best_spot2.y, shape_arr) + + # Place object a 3rd time + best_spot3 = ar.bestSpot(shape_arr) + assert best_spot3.x is not None # we found a location + assert best_spot3.x != best_spot.x or best_spot3.y != best_spot.y # it can't be on the same location + assert best_spot3.x != best_spot2.x or best_spot3.y != best_spot2.y # it can't be on the same location + ar.place(best_spot3.x, best_spot3.y, shape_arr) + + best_spot_x = ar.bestSpot(shape_arr) + ar.place(best_spot_x.x, best_spot_x.y, shape_arr) + + best_spot_x = ar.bestSpot(shape_arr) + ar.place(best_spot_x.x, best_spot_x.y, shape_arr) + + best_spot_x = ar.bestSpot(shape_arr) + ar.place(best_spot_x.x, best_spot_x.y, shape_arr) + + print(ar._occupied) # For debugging + + +## Real life test +def test_bestSpot_scale(): + scale = 0.5 + ar = Arrange(16, 16, 8, 8, scale = scale) + ar.centerFirst() + + shape_arr = gimmeShapeArray(scale) + best_spot = ar.bestSpot(shape_arr) + assert best_spot.x == 0 + assert best_spot.y == 0 + ar.place(best_spot.x, best_spot.y, shape_arr) + + print(ar._occupied) + + # Place object a second time + best_spot = ar.bestSpot(shape_arr) + assert best_spot.x is not None # we found a location + assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location + ar.place(best_spot.x, best_spot.y, shape_arr) + + print(ar._occupied) # For debugging + + +## Real life test +def test_bestSpot_scale_rectangular(): + scale = 0.5 + ar = Arrange(16, 40, 8, 20, scale = scale) + ar.centerFirst() + + shape_arr = gimmeShapeArray(scale) + + shape_arr_square = gimmeShapeArraySquare(scale) + best_spot = ar.bestSpot(shape_arr_square) + assert best_spot.x == 0 + assert best_spot.y == 0 + ar.place(best_spot.x, best_spot.y, shape_arr_square) + + print(ar._occupied) + + # Place object a second time + best_spot = ar.bestSpot(shape_arr) + assert best_spot.x is not None # we found a location + assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location + ar.place(best_spot.x, best_spot.y, shape_arr) + + best_spot = ar.bestSpot(shape_arr_square) + ar.place(best_spot.x, best_spot.y, shape_arr_square) + + print(ar._occupied) # For debugging + + ## Try to place an object and see if something explodes def test_smoke_place(): ar = Arrange(30, 30, 15, 15) @@ -80,6 +254,20 @@ def test_checkShape(): assert points3 > points +## See of our center has less penalty points than out of the center +def test_checkShape_rectangular(): + ar = Arrange(20, 30, 10, 15) + ar.centerFirst() + print(ar._priority) + + shape_arr = gimmeShapeArray() + points = ar.checkShape(0, 0, shape_arr) + points2 = ar.checkShape(5, 0, shape_arr) + points3 = ar.checkShape(0, 5, shape_arr) + assert points2 > points + assert points3 > points + + ## Check that placing an object on occupied place returns None. def test_checkShape_place(): ar = Arrange(30, 30, 15, 15) @@ -104,6 +292,13 @@ def test_smoke_place_objects(): ar.place(best_spot_x, best_spot_y, shape_arr) +# Test some internals +def test_compare_occupied_and_priority_tables(): + ar = Arrange(10, 15, 5, 7) + ar.centerFirst() + assert ar._priority.shape == ar._occupied.shape + + ## Polygon -> array def test_arrayFromPolygon(): vertices = numpy.array([[-3, 1], [3, 1], [0, -3]]) @@ -145,3 +340,5 @@ def test_check2(): assert numpy.any(check_array) assert not check_array[3][0] assert check_array[3][4] + + From 2bbcb2dd7d80cb10369fdcc1255d5aa0c79bd550 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 22 May 2018 17:45:30 +0200 Subject: [PATCH 43/61] Save settings before moving config file on Linux --- cura/Backups/Backup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 65d8f184ec..89a3a54b59 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -43,6 +43,9 @@ class Backup: Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) + # Ensure all current settings are saved. + CuraApplication.getInstance().saveSettings() + # We copy the preferences file to the user data directory in Linux as it's in a different location there. # When restoring a backup on Linux, we move it back. if Platform.isLinux(): @@ -52,9 +55,6 @@ class Backup: Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file) shutil.copyfile(preferences_file, backup_preferences_file) - # Ensure all current settings are saved. - CuraApplication.getInstance().saveSettings() - # Create an empty buffer and write the archive to it. buffer = io.BytesIO() archive = self._makeArchive(buffer, version_data_dir) From f67261488fea54e0eee30a3dfdf5ce550abafc80 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 22 May 2018 22:24:57 +0200 Subject: [PATCH 44/61] Select models after loading Fixes #356 --- cura/CuraApplication.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index ccc2b9f8c4..010b7542aa 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1572,6 +1572,8 @@ class CuraApplication(QtApplication): self.callLater(self.openProjectFile.emit, file) return + Selection.clear() + f = file.toLocalFile() extension = os.path.splitext(f)[1] filename = os.path.basename(f) @@ -1696,6 +1698,8 @@ class CuraApplication(QtApplication): node.callDecoration("setActiveExtruder", default_extruder_id) scene.sceneChanged.emit(node) + Selection.add(node) + self.fileCompleted.emit(filename) def addNonSliceableExtension(self, extension): From 92fa725ad60923d14771c5b7a06d2e89f9144d02 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 23 May 2018 10:00:19 +0200 Subject: [PATCH 45/61] Application.saveSettings() should save everything --- cura/CuraApplication.py | 3 +++ plugins/AutoSave/AutoSave.py | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 2e7eddd8fc..d1c7950884 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -505,6 +505,9 @@ class CuraApplication(QtApplication): return ContainerRegistry.getInstance().saveDirtyContainers() + Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences, + self._application_name + ".cfg")) + def saveStack(self, stack): ContainerRegistry.getInstance().saveContainer(stack) diff --git a/plugins/AutoSave/AutoSave.py b/plugins/AutoSave/AutoSave.py index 5fdac502b5..792e41ffd0 100644 --- a/plugins/AutoSave/AutoSave.py +++ b/plugins/AutoSave/AutoSave.py @@ -72,6 +72,4 @@ class AutoSave(Extension): Application.getInstance().saveSettings() - Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences, Application.getInstance().getApplicationName() + ".cfg")) - self._saving = False From fb8f73556677af4c0f5cc66f5d4d9398d81d47d1 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 23 May 2018 10:05:08 +0200 Subject: [PATCH 46/61] Make selecting objects on load optional and opt-in --- cura/CuraApplication.py | 7 +++++-- resources/qml/Preferences/GeneralPage.qml | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 010b7542aa..024d0403e6 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -377,6 +377,7 @@ class CuraApplication(QtApplication): preferences.addPreference("cura/categories_expanded", "") preferences.addPreference("cura/jobname_prefix", True) + preferences.addPreference("cura/select_models_on_load", False) preferences.addPreference("view/center_on_select", False) preferences.addPreference("mesh/scale_to_fit", False) preferences.addPreference("mesh/scale_tiny_meshes", True) @@ -1572,7 +1573,8 @@ class CuraApplication(QtApplication): self.callLater(self.openProjectFile.emit, file) return - Selection.clear() + if Preferences.getInstance().getValue("cura/select_models_on_load"): + Selection.clear() f = file.toLocalFile() extension = os.path.splitext(f)[1] @@ -1698,7 +1700,8 @@ class CuraApplication(QtApplication): node.callDecoration("setActiveExtruder", default_extruder_id) scene.sceneChanged.emit(node) - Selection.add(node) + if Preferences.getInstance().getValue("cura/select_models_on_load"): + Selection.add(node) self.fileCompleted.emit(filename) diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index f4fa7c1557..7841c7d506 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -79,6 +79,8 @@ UM.PreferencesPage scaleToFitCheckbox.checked = boolCheck(UM.Preferences.getValue("mesh/scale_to_fit")) UM.Preferences.resetPreference("mesh/scale_tiny_meshes") scaleTinyCheckbox.checked = boolCheck(UM.Preferences.getValue("mesh/scale_tiny_meshes")) + UM.Preferences.resetPreference("cura/select_models_on_load") + selectModelsOnLoadCheckbox.checked = boolCheck(UM.Preferences.getValue("cura/select_models_on_load")) UM.Preferences.resetPreference("cura/jobname_prefix") prefixJobNameCheckbox.checked = boolCheck(UM.Preferences.getValue("cura/jobname_prefix")) UM.Preferences.resetPreference("view/show_overhang"); @@ -498,6 +500,21 @@ UM.PreferencesPage } } + UM.TooltipArea + { + width: childrenRect.width + height: childrenRect.height + text: catalog.i18nc("@info:tooltip","Should models be selected after they are loaded?") + + CheckBox + { + id: selectModelsOnLoadCheckbox + text: catalog.i18nc("@option:check","Select models when loaded") + checked: boolCheck(UM.Preferences.getValue("cura/select_models_on_load")) + onCheckedChanged: UM.Preferences.setValue("cura/select_models_on_load", checked) + } + } + UM.TooltipArea { width: childrenRect.width From 6b0045265c6590d041cddd8ad1e569f2c484cc56 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 23 May 2018 10:07:17 +0200 Subject: [PATCH 47/61] Move getting preference out of for loop --- cura/CuraApplication.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 024d0403e6..0b5cb4480c 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1627,6 +1627,8 @@ class CuraApplication(QtApplication): default_extruder_position = self.getMachineManager().defaultExtruderPosition default_extruder_id = self._global_container_stack.extruders[default_extruder_position].getId() + select_models_on_load = Preferences.getInstance().getValue("cura/select_models_on_load") + for original_node in nodes: # Create a CuraSceneNode just if the original node is not that type @@ -1700,7 +1702,7 @@ class CuraApplication(QtApplication): node.callDecoration("setActiveExtruder", default_extruder_id) scene.sceneChanged.emit(node) - if Preferences.getInstance().getValue("cura/select_models_on_load"): + if select_models_on_load: Selection.add(node) self.fileCompleted.emit(filename) From cfd1b7b813ccdac76c623243a03633b2059d9a0e Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 23 May 2018 11:24:33 +0200 Subject: [PATCH 48/61] Fix AutoSave conflicts with BackupManager - Move AutoSave out of the plugins. It's a built-in module now. - Add enable/disable saving data on CuraApplication. - Avoid saving data in backup restore --- {plugins/AutoSave => cura}/AutoSave.py | 34 +++++--------------------- cura/Backups/BackupsManager.py | 2 ++ cura/CuraApplication.py | 16 ++++++++++-- plugins/AutoSave/__init__.py | 13 ---------- plugins/AutoSave/plugin.json | 8 ------ 5 files changed, 22 insertions(+), 51 deletions(-) rename {plugins/AutoSave => cura}/AutoSave.py (56%) delete mode 100644 plugins/AutoSave/__init__.py delete mode 100644 plugins/AutoSave/plugin.json diff --git a/plugins/AutoSave/AutoSave.py b/cura/AutoSave.py similarity index 56% rename from plugins/AutoSave/AutoSave.py rename to cura/AutoSave.py index 792e41ffd0..71e889a62b 100644 --- a/plugins/AutoSave/AutoSave.py +++ b/cura/AutoSave.py @@ -3,17 +3,13 @@ from PyQt5.QtCore import QTimer -from UM.Extension import Extension from UM.Preferences import Preferences -from UM.Application import Application -from UM.Resources import Resources from UM.Logger import Logger -class AutoSave(Extension): - def __init__(self): - super().__init__() - +class AutoSave: + def __init__(self, application): + self._application = application Preferences.getInstance().preferenceChanged.connect(self._triggerTimer) self._global_stack = None @@ -26,29 +22,11 @@ class AutoSave(Extension): self._saving = False - # At this point, the Application instance has not finished its constructor call yet, so directly using something - # like Application.getInstance() is not correct. The initialisation now will only gets triggered after the - # application finishes its start up successfully. - self._init_timer = QTimer() - self._init_timer.setInterval(1000) - self._init_timer.setSingleShot(True) - self._init_timer.timeout.connect(self.initialize) - self._init_timer.start() - def initialize(self): # only initialise if the application is created and has started - from cura.CuraApplication import CuraApplication - if not CuraApplication.Created: - self._init_timer.start() - return - if not CuraApplication.getInstance().started: - self._init_timer.start() - return - self._change_timer.timeout.connect(self._onTimeout) - Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) + self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() - self._triggerTimer() def _triggerTimer(self, *args): @@ -60,7 +38,7 @@ class AutoSave(Extension): self._global_stack.propertyChanged.disconnect(self._triggerTimer) self._global_stack.containersChanged.disconnect(self._triggerTimer) - self._global_stack = Application.getInstance().getGlobalContainerStack() + self._global_stack = self._application.getGlobalContainerStack() if self._global_stack: self._global_stack.propertyChanged.connect(self._triggerTimer) @@ -70,6 +48,6 @@ class AutoSave(Extension): self._saving = True # To prevent the save process from triggering another autosave. Logger.log("d", "Autosaving preferences, instances and profiles") - Application.getInstance().saveSettings() + self._application.saveSettings() self._saving = False diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 04955692ee..38ffcac92b 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -48,7 +48,9 @@ class BackupsManager: def _disableAutoSave(self): """Here we try to disable the auto-save plugin as it might interfere with restoring a backup.""" # TODO: Disable auto-save if possible. + CuraApplication.getInstance().setSaveDataEnabled(False) def _enableAutoSave(self): """Re-enable auto-save after we're done.""" # TODO: Enable auto-save if possible. + CuraApplication.getInstance().setSaveDataEnabled(True) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index d1c7950884..5b191c9180 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -74,6 +74,7 @@ from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager from cura.Machines.VariantManager import VariantManager +from .AutoSave import AutoSave from . import PlatformPhysics from . import BuildVolume from . import CameraAnimation @@ -234,6 +235,8 @@ class CuraApplication(QtApplication): self._simple_mode_settings_manager = None self._cura_scene_controller = None self._machine_error_checker = None + self._auto_save = None + self._save_data_enabled = True self._additional_components = {} # Components to add to certain areas in the interface @@ -496,11 +499,14 @@ class CuraApplication(QtApplication): showPrintMonitor = pyqtSignal(bool, arguments = ["show"]) + def setSaveDataEnabled(self, enabled: bool) -> None: + self._save_data_enabled = enabled + ## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently. # # Note that the AutoSave plugin also calls this method. - def saveSettings(self, safe_data: bool = True): - if not self.started or not safe_data: + def saveSettings(self): + if not self.started or not self._save_data_enabled: # Do not do saving during application start or when data should not be safed on quit. return ContainerRegistry.getInstance().saveDirtyContainers() @@ -728,6 +734,9 @@ class CuraApplication(QtApplication): self._post_start_timer.timeout.connect(self._onPostStart) self._post_start_timer.start() + self._auto_save = AutoSave(self) + self._auto_save.initialize() + self.exec_() def _onPostStart(self): @@ -879,6 +888,9 @@ class CuraApplication(QtApplication): return super().event(event) + def getAutoSave(self): + return self._auto_save + ## Get print information (duration / material used) def getPrintInformation(self): return self._print_information diff --git a/plugins/AutoSave/__init__.py b/plugins/AutoSave/__init__.py deleted file mode 100644 index d7ee0736a2..0000000000 --- a/plugins/AutoSave/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright (c) 2016 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -from . import AutoSave - -from UM.i18n import i18nCatalog -catalog = i18nCatalog("cura") - -def getMetaData(): - return {} - -def register(app): - return { "extension": AutoSave.AutoSave() } diff --git a/plugins/AutoSave/plugin.json b/plugins/AutoSave/plugin.json deleted file mode 100644 index 32e07a1062..0000000000 --- a/plugins/AutoSave/plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "Auto Save", - "author": "Ultimaker B.V.", - "version": "1.0.0", - "description": "Automatically saves Preferences, Machines and Profiles after changes.", - "api": 4, - "i18n-catalog": "cura" -} From a3ac20172bcabf2baf297c651b215e25cce8caf1 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 23 May 2018 12:54:18 +0200 Subject: [PATCH 49/61] CURA-5370 The minimum offset between objects in the Arranger is now determined by the brim/skirt/... setting --- cura/BuildVolume.py | 8 +++--- cura/CuraActions.py | 3 ++- cura/CuraApplication.py | 4 +-- tests/TestArrange.py | 55 +++++++++++++++++++++++++++++++++-------- 4 files changed, 53 insertions(+), 17 deletions(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index ea0d8e1565..487c5268fc 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -460,7 +460,7 @@ class BuildVolume(SceneNode): minimum = Vector(min_w, min_h - 1.0, min_d), maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d)) - bed_adhesion_size = self._getEdgeDisallowedSize() + bed_adhesion_size = self.getEdgeDisallowedSize() # As this works better for UM machines, we only add the disallowed_area_size for the z direction. # This is probably wrong in all other cases. TODO! @@ -652,7 +652,7 @@ class BuildVolume(SceneNode): extruder_manager = ExtruderManager.getInstance() used_extruders = extruder_manager.getUsedExtruderStacks() - disallowed_border_size = self._getEdgeDisallowedSize() + disallowed_border_size = self.getEdgeDisallowedSize() if not used_extruders: # If no extruder is used, assume that the active extruder is used (else nothing is drawn) @@ -962,12 +962,12 @@ class BuildVolume(SceneNode): all_values[i] = 0 return all_values - ## Convenience function to calculate the disallowed radius around the edge. + ## Calculate the disallowed radius around the edge. # # This disallowed radius is to allow for space around the models that is # not part of the collision radius, such as bed adhesion (skirt/brim/raft) # and travel avoid distance. - def _getEdgeDisallowedSize(self): + def getEdgeDisallowedSize(self): if not self._global_container_stack or not self._global_container_stack.extruders: return 0 diff --git a/cura/CuraActions.py b/cura/CuraActions.py index 75338f17b6..92c0e8ae1c 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -73,7 +73,8 @@ class CuraActions(QObject): # \param count The number of times to multiply the selection. @pyqtSlot(int) def multiplySelection(self, count: int) -> None: - job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = 8) + min_offset = Application.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors + job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8)) job.start() ## Delete all selected objects. diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 9c752cb7a2..0ac7dcf018 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1264,7 +1264,8 @@ class CuraApplication(QtApplication): # \param nodes nodes that we have to place # \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes def arrange(self, nodes, fixed_nodes): - job = ArrangeObjectsJob(nodes, fixed_nodes) + min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors + job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8)) job.start() ## Reload all mesh data on the screen from file. @@ -1613,7 +1614,6 @@ class CuraApplication(QtApplication): if(original_node.getScale() != Vector(1.0, 1.0, 1.0)): node.scale(original_node.getScale()) - node.setSelectable(True) node.setName(os.path.basename(filename)) self.getBuildVolume().checkBoundsAndUpdate(node) diff --git a/tests/TestArrange.py b/tests/TestArrange.py index 354bbf4962..f383fc0cf3 100755 --- a/tests/TestArrange.py +++ b/tests/TestArrange.py @@ -4,16 +4,26 @@ from cura.Arranging.Arrange import Arrange from cura.Arranging.ShapeArray import ShapeArray +## Triangle of area 12 +def gimmeTriangle(): + return numpy.array([[-3, 1], [3, 1], [0, -3]], dtype=numpy.int32) + + +## Boring square +def gimmeSquare(): + return numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32) + + ## Triangle of area 12 def gimmeShapeArray(scale = 1.0): - vertices = numpy.array([[-3, 1], [3, 1], [0, -3]], dtype=numpy.int32) + vertices = gimmeTriangle() shape_arr = ShapeArray.fromPolygon(vertices, scale = scale) return shape_arr ## Boring square def gimmeShapeArraySquare(scale = 1.0): - vertices = numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32) + vertices = gimmeSquare() shape_arr = ShapeArray.fromPolygon(vertices, scale = scale) return shape_arr @@ -69,7 +79,7 @@ def test_ShapeArray_scaling2(): ## Test centerFirst def test_centerFirst(): - ar = Arrange(300, 300, 150, 150) + ar = Arrange(300, 300, 150, 150, scale = 1) ar.centerFirst() assert ar._priority[150][150] < ar._priority[170][150] assert ar._priority[150][150] < ar._priority[150][170] @@ -81,7 +91,7 @@ def test_centerFirst(): ## Test centerFirst def test_centerFirst_rectangular(): - ar = Arrange(400, 300, 200, 150) + ar = Arrange(400, 300, 200, 150, scale = 1) ar.centerFirst() assert ar._priority[150][200] < ar._priority[150][220] assert ar._priority[150][200] < ar._priority[170][200] @@ -93,7 +103,7 @@ def test_centerFirst_rectangular(): ## Test centerFirst def test_centerFirst_rectangular(): - ar = Arrange(10, 20, 5, 10) + ar = Arrange(10, 20, 5, 10, scale = 1) ar.centerFirst() print(ar._priority) assert ar._priority[10][5] < ar._priority[10][7] @@ -101,7 +111,7 @@ def test_centerFirst_rectangular(): ## Test backFirst def test_backFirst(): - ar = Arrange(300, 300, 150, 150) + ar = Arrange(300, 300, 150, 150, scale = 1) ar.backFirst() assert ar._priority[150][150] < ar._priority[170][150] assert ar._priority[150][150] < ar._priority[170][170] @@ -111,7 +121,7 @@ def test_backFirst(): ## See if the result of bestSpot has the correct form def test_smoke_bestSpot(): - ar = Arrange(30, 30, 15, 15) + ar = Arrange(30, 30, 15, 15, scale = 1) ar.centerFirst() shape_arr = gimmeShapeArray() @@ -124,7 +134,7 @@ def test_smoke_bestSpot(): ## Real life test def test_bestSpot(): - ar = Arrange(16, 16, 8, 8) + ar = Arrange(16, 16, 8, 8, scale = 1) ar.centerFirst() shape_arr = gimmeShapeArray() @@ -144,7 +154,7 @@ def test_bestSpot(): ## Real life test rectangular build plate def test_bestSpot_rectangular_build_plate(): - ar = Arrange(16, 40, 8, 20) + ar = Arrange(16, 40, 8, 20, scale = 1) ar.centerFirst() shape_arr = gimmeShapeArray() @@ -283,7 +293,7 @@ def test_checkShape_place(): ## Test the whole sequence def test_smoke_place_objects(): - ar = Arrange(20, 20, 10, 10) + ar = Arrange(20, 20, 10, 10, scale = 1) ar.centerFirst() shape_arr = gimmeShapeArray() @@ -342,3 +352,28 @@ def test_check2(): assert check_array[3][4] +## Just adding some stuff to ensure fromNode works as expected. Some parts should actually be in UM +def test_parts_of_fromNode(): + from UM.Math.Polygon import Polygon + p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32)) + offset = 1 + print(p._points) + p_offset = p.getMinkowskiHull(Polygon.approximatedCircle(offset)) + print("--------------") + print(p_offset._points) + assert len(numpy.where(p_offset._points[:, 0] >= 2.9)) > 0 + assert len(numpy.where(p_offset._points[:, 0] <= -2.9)) > 0 + assert len(numpy.where(p_offset._points[:, 1] >= 2.9)) > 0 + assert len(numpy.where(p_offset._points[:, 1] <= -2.9)) > 0 + + +def test_parts_of_fromNode2(): + from UM.Math.Polygon import Polygon + p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32) * 2) # 4x4 + offset = 13.3 + scale = 0.5 + p_offset = p.getMinkowskiHull(Polygon.approximatedCircle(offset)) + shape_arr1 = ShapeArray.fromPolygon(p._points, scale = scale) + shape_arr2 = ShapeArray.fromPolygon(p_offset._points, scale = scale) + assert shape_arr1.arr.shape[0] >= (4 * scale) - 1 # -1 is to account for rounding errors + assert shape_arr2.arr.shape[0] >= (2 * offset + 4) * scale - 1 From de455a9a64043f3f0510dcb581ab52e376a6674b Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 23 May 2018 15:48:28 +0200 Subject: [PATCH 50/61] Use gcode.gz as extension for GzWriter CURA-5343 We have the custom save file dialog on OS X now so there is no need to use .gz to bypass the extension problem on the native dialog. --- plugins/GCodeGzWriter/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/GCodeGzWriter/__init__.py b/plugins/GCodeGzWriter/__init__.py index a4d576aef6..e257bcb011 100644 --- a/plugins/GCodeGzWriter/__init__.py +++ b/plugins/GCodeGzWriter/__init__.py @@ -9,7 +9,7 @@ from . import GCodeGzWriter catalog = i18nCatalog("cura") def getMetaData(): - file_extension = "gz" if Platform.isOSX() else "gcode.gz" + file_extension = "gcode.gz" return { "mesh_writer": { "output": [{ From 827cb33b6e2b56b8be9fbb6db59fb93bc2750775 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 23 May 2018 16:04:18 +0200 Subject: [PATCH 51/61] CURA-5370 Update copyright and comments --- cura/Arranging/Arrange.py | 3 +++ cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/cura/Arranging/Arrange.py b/cura/Arranging/Arrange.py index f8c6ae8a31..1027b39199 100644 --- a/cura/Arranging/Arrange.py +++ b/cura/Arranging/Arrange.py @@ -1,3 +1,6 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Logger import Logger from UM.Math.Vector import Vector diff --git a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py index 1918b32907..252cef4e65 100644 --- a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from UM.Application import Application @@ -18,7 +18,7 @@ from cura.Arranging.ShapeArray import ShapeArray from typing import List -## Do an arrangements on a bunch of build plates +## Do arrangements on multiple build plates (aka builtiplexer) class ArrangeArray: def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]): self._x = x From d14f4ac67868a31a5eebe982cdbfcb892c7dbad9 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Wed, 23 May 2018 16:28:35 +0200 Subject: [PATCH 52/61] CURA-5280 Preserve manually entered project name Manual override of job name should also set the base name so that when the printer prefix is updated, it the prefix can be added to the manually added name, not the old base name. This was a bit tricky because if you enter a job name, is that the job name or the base name? My answer is that it's both, and if you update your printer for the job, it will append a new prefix. If this is not the desired behavior, and in fact once a custom name is set nothing can change it, then line 78 should be removed. --- cura/PrintInformation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 01d0b43f5b..a901d326ca 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -279,9 +279,11 @@ class PrintInformation(QObject): for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1): self._calculateInformation(build_plate_number) + # prefix can be added to the manually added name, not the old base name @pyqtSlot(str) def setJobName(self, name): self._job_name = name + self._base_name = name self.jobNameChanged.emit() jobNameChanged = pyqtSignal() From e89bd91960d9d13e0e92c138e6389bfda82ddd05 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Wed, 23 May 2018 16:31:22 +0200 Subject: [PATCH 53/61] CURA-5280 Preserve manually entered project name Heeeehhhhh? Why didn't this line get staged with the file? --- cura/PrintInformation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index a901d326ca..8768f850b9 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -279,6 +279,7 @@ class PrintInformation(QObject): for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1): self._calculateInformation(build_plate_number) + # Manual override of job name should also set the base name so that when the printer prefix is updated, it the # prefix can be added to the manually added name, not the old base name @pyqtSlot(str) def setJobName(self, name): From 196bd3a73042d097e78f46a9abf968c042ab24cf Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 23 May 2018 16:31:29 +0200 Subject: [PATCH 54/61] Add non-thumbnail-visible flag for creating previews CURA-5373 Addition to the non-slicable meshes, the support meshes should also not be included in the previews. --- cura/PreviewPass.py | 5 ++--- cura/Settings/SettingOverrideDecorator.py | 10 ++++++++++ cura/Snapshot.py | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/cura/PreviewPass.py b/cura/PreviewPass.py index 4241a2f243..436e2719b7 100644 --- a/cura/PreviewPass.py +++ b/cura/PreviewPass.py @@ -79,10 +79,10 @@ class PreviewPass(RenderPass): for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): per_mesh_stack = node.callDecoration("getStack") - if node.callDecoration("isNonPrintingMesh"): + if node.callDecoration("isNonThumbnailVisibleMesh"): # Non printing mesh continue - elif per_mesh_stack is not None and per_mesh_stack.getProperty("support_mesh", "value") == True: + elif per_mesh_stack is not None and per_mesh_stack.getProperty("support_mesh", "value"): # Support mesh uniforms = {} shade_factor = 0.6 @@ -112,4 +112,3 @@ class PreviewPass(RenderPass): batch_support_mesh.render(render_camera) self.release() - diff --git a/cura/Settings/SettingOverrideDecorator.py b/cura/Settings/SettingOverrideDecorator.py index 69d98314b6..a662027d8f 100644 --- a/cura/Settings/SettingOverrideDecorator.py +++ b/cura/Settings/SettingOverrideDecorator.py @@ -30,6 +30,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): # Note that Support Mesh is not in here because it actually generates # g-code in the volume of the mesh. _non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"} + _non_thumbnail_visible_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"} def __init__(self): super().__init__() @@ -41,6 +42,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): self._extruder_stack = ExtruderManager.getInstance().getExtruderStack(0).getId() self._is_non_printing_mesh = False + self._is_non_thumbnail_visible_mesh = False self._stack.propertyChanged.connect(self._onSettingChanged) @@ -72,6 +74,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): # use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh" # has not been updated yet. deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh() + deep_copy._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh() return deep_copy @@ -102,10 +105,17 @@ class SettingOverrideDecorator(SceneNodeDecorator): def evaluateIsNonPrintingMesh(self): return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings) + def isNonThumbnailVisibleMesh(self): + return self._is_non_thumbnail_visible_mesh + + def evaluateIsNonThumbnailVisibleMesh(self): + return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_thumbnail_visible_settings) + def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function if property_name == "value": # Trigger slice/need slicing if the value has changed. self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh() + self._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh() Application.getInstance().getBackend().needsSlicing() Application.getInstance().getBackend().tickle() diff --git a/cura/Snapshot.py b/cura/Snapshot.py index d1bfeea40f..b730c1fdcf 100644 --- a/cura/Snapshot.py +++ b/cura/Snapshot.py @@ -48,7 +48,7 @@ class Snapshot: # determine zoom and look at bbox = None for node in DepthFirstIterator(root): - if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonPrintingMesh"): + if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonThumbnailVisibleMesh"): if bbox is None: bbox = node.getBoundingBox() else: From a0a8af07dc779af6570c069499b1a7f6fce6e0ad Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 23 May 2018 16:40:49 +0200 Subject: [PATCH 55/61] Upon manual job name change, only save the real base name CURA-5280 Remove the machine prefix in the base name, otherwise when the machine gets changed, the job name will become "__basename". --- cura/PrintInformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 8768f850b9..3260d55c74 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -284,7 +284,7 @@ class PrintInformation(QObject): @pyqtSlot(str) def setJobName(self, name): self._job_name = name - self._base_name = name + self._base_name = name.replace(self._abbr_machine + "_", "") self.jobNameChanged.emit() jobNameChanged = pyqtSignal() From 260dee54fbf9b9e71e3d69e352188c12ba8c7a1a Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 23 May 2018 16:57:50 +0200 Subject: [PATCH 56/61] CURA-5370 also take into account the machine size when Arranging in readMeshFinished --- cura/CuraApplication.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 8457244dff..c1c52c42bf 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1615,7 +1615,10 @@ class CuraApplication(QtApplication): for node_ in DepthFirstIterator(root): if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate: fixed_nodes.append(node_) - arranger = Arrange.create(fixed_nodes = fixed_nodes) + global_container_stack = self.getGlobalContainerStack() + machine_width = global_container_stack.getProperty("machine_width", "value") + machine_depth = global_container_stack.getProperty("machine_depth", "value") + arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes) min_offset = 8 default_extruder_position = self.getMachineManager().defaultExtruderPosition default_extruder_id = self._global_container_stack.extruders[default_extruder_position].getId() From 809cab4ea55795fec01070a36d8d79b07ec30312 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 23 May 2018 19:18:11 +0200 Subject: [PATCH 57/61] Cleanup for cfd1b7b813ccdac76c623243a03633b2059d9a0e --- cura/Backups/Backup.py | 1 - cura/Backups/BackupsManager.py | 10 +++++----- cura/CuraApplication.py | 5 +---- resources/bundled_packages.json | 17 ----------------- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 89a3a54b59..c4fe720b2b 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -14,7 +14,6 @@ from UM.Logger import Logger from UM.Message import Message from UM.Platform import Platform from UM.Resources import Resources -from UM.Version import Version from cura.CuraApplication import CuraApplication diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 38ffcac92b..fa75ddb587 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -12,6 +12,8 @@ class BackupsManager: The BackupsManager is responsible for managing the creating and restoring of backups. Backups themselves are represented in a different class. """ + def __init__(self): + self._application = CuraApplication.getInstance() def createBackup(self) -> (Optional[bytes], Optional[dict]): """ @@ -43,14 +45,12 @@ class BackupsManager: if restored: # At this point, Cura will need to restart for the changes to take effect. # We don't want to store the data at this point as that would override the just-restored backup. - CuraApplication.getInstance().windowClosed(save_data=False) + self._application.windowClosed(save_data=False) def _disableAutoSave(self): """Here we try to disable the auto-save plugin as it might interfere with restoring a backup.""" - # TODO: Disable auto-save if possible. - CuraApplication.getInstance().setSaveDataEnabled(False) + self._application.setSaveDataEnabled(False) def _enableAutoSave(self): """Re-enable auto-save after we're done.""" - # TODO: Enable auto-save if possible. - CuraApplication.getInstance().setSaveDataEnabled(True) + self._application.setSaveDataEnabled(True) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index c1c52c42bf..5db616ab3b 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -503,15 +503,12 @@ class CuraApplication(QtApplication): def setSaveDataEnabled(self, enabled: bool) -> None: self._save_data_enabled = enabled - ## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently. - # - # Note that the AutoSave plugin also calls this method. + # Cura has multiple locations where instance containers need to be saved, so we need to handle this differently. def saveSettings(self): if not self.started or not self._save_data_enabled: # Do not do saving during application start or when data should not be safed on quit. return ContainerRegistry.getInstance().saveDirtyContainers() - Preferences.getInstance().writeToFile(Resources.getStoragePath(Resources.Preferences, self._application_name + ".cfg")) diff --git a/resources/bundled_packages.json b/resources/bundled_packages.json index 8d58f226b0..a63d08ddab 100644 --- a/resources/bundled_packages.json +++ b/resources/bundled_packages.json @@ -33,23 +33,6 @@ } } }, - "AutoSave": { - "package_info": { - "package_id": "AutoSave", - "package_type": "plugin", - "display_name": "Auto-Save", - "description": "Automatically saves Preferences, Machines and Profiles after changes.", - "package_version": "1.0.0", - "cura_version": 4, - "website": "https://ultimaker.com", - "author": { - "author_id": "Ultimaker", - "display_name": "Ultimaker B.V.", - "email": "plugins@ultimaker.com", - "website": "https://ultimaker.com" - } - } - }, "ChangeLogPlugin": { "package_info": { "package_id": "ChangeLogPlugin", From 2422ee38e80f6a7672b64f00978228668bd034e2 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 24 May 2018 09:46:18 +0200 Subject: [PATCH 58/61] Fix init py in backups module --- cura/Backups/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 cura/Backups/__init__.py diff --git a/cura/Backups/__init__.py b/cura/Backups/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From 3045007f5b6428be97a9557fc3959622c97b55cd Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 24 May 2018 10:39:37 +0200 Subject: [PATCH 59/61] Small codestyle changes after merge --- plugins/MonitorStage/MonitorStage.py | 6 ++++-- plugins/PrepareStage/PrepareStage.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py index 631da71072..ace201e994 100644 --- a/plugins/MonitorStage/MonitorStage.py +++ b/plugins/MonitorStage/MonitorStage.py @@ -69,9 +69,11 @@ class MonitorStage(CuraStage): self._updateSidebar() def _updateMainOverlay(self): - main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml") + main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), + "MonitorMainView.qml") self.addDisplayComponent("main", main_component_path) def _updateSidebar(self): - sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "MonitorSidebar.qml") + sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), + "MonitorSidebar.qml") self.addDisplayComponent("sidebar", sidebar_component_path) diff --git a/plugins/PrepareStage/PrepareStage.py b/plugins/PrepareStage/PrepareStage.py index df40476f7f..c3c9f0a1f8 100644 --- a/plugins/PrepareStage/PrepareStage.py +++ b/plugins/PrepareStage/PrepareStage.py @@ -14,5 +14,6 @@ class PrepareStage(CuraStage): Application.getInstance().engineCreatedSignal.connect(self._engineCreated) def _engineCreated(self): - sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "PrepareSidebar.qml") + sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), + "PrepareSidebar.qml") self.addDisplayComponent("sidebar", sidebar_component_path) From 717c11005b26a6dff2218937c1e91d9f715ba60b Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 24 May 2018 10:58:55 +0200 Subject: [PATCH 60/61] Codestyle changes after merge --- .../scripts/PauseAtHeight.py | 44 +++++-------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py index 4795aa3e89..cb31514e9d 100644 --- a/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py +++ b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py @@ -117,34 +117,21 @@ class PauseAtHeight(Script): } }""" - def getNextXY(self, layer: str): - """ Get the X and Y values for a layer (will be used to get X and Y of the layer after the pause """ - lines = layer.split("\n") - for line in lines: - if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None: - x = self.getValue(line, "X") y = self.getValue(line, "Y") - - return (x, y) - - return (0, 0) - + return x, y + return 0, 0 def execute(self, data: list): - """data is a list. Each index contains a layer""" - - x = 0. - y = 0. pause_at = self.getSettingValueByKey("pause_at") pause_height = self.getSettingValueByKey("pause_height") pause_layer = self.getSettingValueByKey("pause_layer") @@ -173,15 +160,12 @@ class PauseAtHeight(Script): # Scroll each line of instruction for each layer in the G-code for line in lines: - # Fist positive layer reached if ";LAYER:0" in line: layers_started = True - # Count nbr of negative layers (raft) elif ";LAYER:-" in line: nbr_negative_layers += 1 - if not layers_started: continue @@ -190,7 +174,6 @@ class PauseAtHeight(Script): current_z = self.getValue(line, "Z") if pause_at == "height": - # Ignore if the line is not G1 or G0 if self.getValue(line, "G") != 1 and self.getValue(line, "G") != 0: continue @@ -208,7 +191,6 @@ class PauseAtHeight(Script): # Pause at layer else: - if not line.startswith(";LAYER:"): continue current_layer = line[len(";LAYER:"):] @@ -224,36 +206,35 @@ class PauseAtHeight(Script): # Get X and Y from the next layer (better position for # the nozzle) - nextLayer = data[index + 1] - x, y = self.getNextXY(nextLayer) + next_layer = data[index + 1] + x, y = self.getNextXY(next_layer) - prevLayer = data[index - 1] - prevLines = prevLayer.split("\n") + prev_layer = data[index - 1] + prev_lines = prev_layer.split("\n") current_e = 0. # Access last layer, browse it backwards to find # last extruder absolute position - for prevLine in reversed(prevLines): + for prevLine in reversed(prev_lines): current_e = self.getValue(prevLine, "E", -1) if current_e >= 0: break # include a number of previous layers for i in range(1, redo_layers + 1): - prevLayer = data[index - i] - layer = prevLayer + layer + prev_layer = data[index - i] + layer = prev_layer + layer # Get extruder's absolute position at the - # begining of the first layer redone + # beginning of the first layer redone # see https://github.com/nallath/PostProcessingPlugin/issues/55 if i == redo_layers: # Get X and Y from the next layer (better position for # the nozzle) x, y = self.getNextXY(layer) - prevLines = prevLayer.split("\n") - for line in prevLines: + prev_lines = prev_layer.split("\n") + for line in prev_lines: new_e = self.getValue(line, 'E', current_e) - if new_e != current_e: current_e = new_e break @@ -319,7 +300,6 @@ class PauseAtHeight(Script): layer = prepend_gcode + layer - # Override the data of this layer with the # modified data data[index] = layer From 26e32dc08a332ffcb63b9ca4a1262ac8ef63c685 Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Thu, 24 May 2018 11:46:46 +0200 Subject: [PATCH 61/61] Added missing colon --- plugins/USBPrinting/USBPrinterOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 4f6069d13b..b2ca5562e3 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -318,7 +318,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): continue matched_extruder_nrs.append(extruder_nr) - if extruder_nr >= len(self._printers[0].extruders) + if extruder_nr >= len(self._printers[0].extruders): Logger.log("w", "Printer reports more temperatures than the number of configured extruders") continue