diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 3286a69bbb..0e626b82f0 100644 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -223,6 +223,10 @@ class CuraApplication(QtApplication): Preferences.getInstance().addPreference("mesh/scale_tiny_meshes", True) Preferences.getInstance().addPreference("cura/dialog_on_project_save", True) Preferences.getInstance().addPreference("cura/asked_dialog_on_project_save", False) + + Preferences.getInstance().addPreference("cura/currency", "€") + Preferences.getInstance().addPreference("cura/material_settings", "{}") + for key in [ "dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin "dialog_profile_path", diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 7834ef77bd..769df189c5 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -7,12 +7,14 @@ from UM.FlameProfiler import pyqtSlot from UM.Application import Application from UM.Qt.Duration import Duration from UM.Preferences import Preferences +from UM.Settings import ContainerRegistry import cura.Settings.ExtruderManager import math import os.path import unicodedata +import json from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") @@ -52,6 +54,7 @@ class PrintInformation(QObject): self._material_lengths = [] self._material_weights = [] + self._material_costs = [] self._pre_sliced = False @@ -65,6 +68,12 @@ class PrintInformation(QObject): Application.getInstance().globalContainerStackChanged.connect(self._setAbbreviatedMachineName) Application.getInstance().fileLoaded.connect(self.setJobName) + Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) + + self._active_material_container = None + Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._onActiveMaterialChanged) + self._onActiveMaterialChanged() + currentPrintTimeChanged = pyqtSignal() preSlicedChanged = pyqtSignal() @@ -93,28 +102,82 @@ class PrintInformation(QObject): def materialWeights(self): return self._material_weights + materialCostsChanged = pyqtSignal() + + @pyqtProperty("QVariantList", notify = materialCostsChanged) + def materialCosts(self): + return self._material_costs + def _onPrintDurationMessage(self, total_time, material_amounts): self._current_print_time.setDuration(total_time) self.currentPrintTimeChanged.emit() + self._material_amounts = material_amounts + self._calculateInformation() + + def _calculateInformation(self): # Material amount is sent as an amount of mm^3, so calculate length from that r = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2 self._material_lengths = [] self._material_weights = [] + self._material_costs = [] + + material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings")) + extruder_stacks = list(cura.Settings.ExtruderManager.getInstance().getMachineExtruders(Application.getInstance().getGlobalContainerStack().getId())) - for index, amount in enumerate(material_amounts): + for index, amount in enumerate(self._material_amounts): ## Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some # list comprehension filtering to solve this for us. + material = None if extruder_stacks: # Multi extrusion machine extruder_stack = [extruder for extruder in extruder_stacks if extruder.getMetaDataEntry("position") == str(index)][0] density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0) + material = extruder_stack.findContainer({"type": "material"}) else: # Machine with no extruder stacks density = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("properties", {}).get("density", 0) + material = Application.getInstance().getGlobalContainerStack().findContainer({"type": "material"}) - self._material_weights.append(float(amount) * float(density) / 1000) + weight = float(amount) * float(density) / 1000 + cost = 0 + if material: + material_guid = material.getMetaDataEntry("GUID") + if material_guid in material_preference_values: + material_values = material_preference_values[material_guid] + + weight_per_spool = float(material_values["spool_weight"] if material_values and "spool_weight" in material_values else 0) + cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0) + + if weight_per_spool != 0: + cost = cost_per_spool * weight / weight_per_spool + else: + cost = 0 + + self._material_weights.append(weight) self._material_lengths.append(round((amount / (math.pi * r ** 2)) / 1000, 2)) + self._material_costs.append(cost) + self.materialLengthsChanged.emit() self.materialWeightsChanged.emit() + self.materialCostsChanged.emit() + + def _onPreferencesChanged(self, preference): + if preference != "cura/material_settings": + return + + self._calculateInformation() + + def _onActiveMaterialChanged(self): + if self._active_material_container: + self._active_material_container.metaDataChanged.disconnect(self._onMaterialMetaDataChanged) + + active_material_id = Application.getInstance().getMachineManager().activeMaterialId + self._active_material_container = ContainerRegistry.getInstance().findInstanceContainers(id=active_material_id)[0] + + if self._active_material_container: + self._active_material_container.metaDataChanged.connect(self._onMaterialMetaDataChanged) + + def _onMaterialMetaDataChanged(self): + self._calculateInformation() @pyqtSlot(str) def setJobName(self, name): diff --git a/resources/qml/JobSpecs.qml b/resources/qml/JobSpecs.qml index 78f184f13c..00d22ae8a8 100644 --- a/resources/qml/JobSpecs.qml +++ b/resources/qml/JobSpecs.qml @@ -26,6 +26,7 @@ Rectangle { property variant printDuration: PrintInformation.currentPrintTime property variant printMaterialLengths: PrintInformation.materialLengths property variant printMaterialWeights: PrintInformation.materialWeights + property variant printMaterialCosts: PrintInformation.materialCosts height: childrenRect.height color: "transparent" @@ -133,7 +134,8 @@ Rectangle { } } - Label{ + Label + { id: boundingSpec anchors.top: jobNameRow.bottom anchors.right: parent.right @@ -144,17 +146,20 @@ Rectangle { text: Printer.getSceneBoundingBoxString } - Rectangle { + Rectangle + { id: specsRow anchors.top: boundingSpec.bottom anchors.right: parent.right height: UM.Theme.getSize("jobspecs_line").height - Item{ + Item + { width: parent.width height: parent.height - UM.RecolorImage { + UM.RecolorImage + { id: timeIcon anchors.right: timeSpec.left anchors.rightMargin: UM.Theme.getSize("default_margin").width/2 @@ -166,7 +171,8 @@ Rectangle { color: UM.Theme.getColor("text_subtext") source: UM.Theme.getIcon("print_time") } - Label{ + Label + { id: timeSpec anchors.right: lengthIcon.left anchors.rightMargin: UM.Theme.getSize("default_margin").width @@ -175,7 +181,8 @@ Rectangle { color: UM.Theme.getColor("text_subtext") text: (!base.printDuration || !base.printDuration.valid) ? catalog.i18nc("@label", "00h 00min") : base.printDuration.getDisplayString(UM.DurationFormat.Short) } - UM.RecolorImage { + UM.RecolorImage + { id: lengthIcon anchors.right: lengthSpec.left anchors.rightMargin: UM.Theme.getSize("default_margin").width/2 @@ -187,7 +194,8 @@ Rectangle { color: UM.Theme.getColor("text_subtext") source: UM.Theme.getIcon("category_material") } - Label{ + Label + { id: lengthSpec anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -197,19 +205,38 @@ Rectangle { { 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) { + 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.floor(base.printMaterialWeights[index]))); + costs.push(base.printMaterialCosts[index].toFixed(2)); + if(base.printMaterialCosts[index] > 0) + { + someCostsKnown = true; + } } } } - if(lengths.length == 0) { + if(lengths.length == 0) + { lengths = ["0.00"]; weights = ["0"]; + costs = ["0.00"]; + } + if(someCostsKnown) + { + return catalog.i18nc("@label", "%1 m / ~ %2 g / ~ %4 %3").arg(lengths.join(" + ")) + .arg(weights.join(" + ")).arg(costs.join(" + ")).arg(UM.Preferences.getValue("cura/currency")); + } + else + { + return catalog.i18nc("@label", "%1 m / ~ %2 g").arg(lengths.join(" + ")).arg(weights.join(" + ")); } - return catalog.i18nc("@label", "%1 m / ~ %2 g").arg(lengths.join(" + ")).arg(weights.join(" + ")); } } } diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index ad71e9ebfe..c3e4d5d846 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -129,6 +129,19 @@ UM.PreferencesPage currentIndex -= 1; } } + + Label + { + id: currencyLabel + text: catalog.i18nc("@label","Currency:") + anchors.verticalCenter: languageComboBox.verticalCenter + } + TextField + { + id: currencyField + text: UM.Preferences.getValue("cura/currency") + onTextChanged: UM.Preferences.setValue("cura/currency", text) + } } Label diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml index 7ea363454b..17f76466ab 100644 --- a/resources/qml/Preferences/MaterialView.qml +++ b/resources/qml/Preferences/MaterialView.qml @@ -15,14 +15,19 @@ TabView property QtObject properties; property bool editingEnabled: false; - property string currency: UM.Preferences.getValue("general/currency") ? UM.Preferences.getValue("general/currency") : "€" + property string currency: UM.Preferences.getValue("cura/currency") ? UM.Preferences.getValue("cura/currency") : "€" property real firstColumnWidth: width * 0.45 property real secondColumnWidth: width * 0.45 property string containerId: "" + property var materialPreferenceValues: UM.Preferences.getValue("cura/material_settings") ? JSON.parse(UM.Preferences.getValue("cura/material_settings")) : {} + + property double spoolLength: calculateSpoolLength() + property real costPerMeter: calculateCostPerMeter() Tab { title: catalog.i18nc("@title","Information") + anchors { leftMargin: UM.Theme.getSize("default_margin").width @@ -35,6 +40,7 @@ TabView { anchors.fill: parent horizontalScrollBarPolicy: Qt.ScrollBarAlwaysOff + flickableItem.flickableDirection: Flickable.VerticalFlick Flow { @@ -112,64 +118,78 @@ TabView Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Density") } ReadOnlySpinBox { - width: base.secondColumnWidth; - value: properties.density; + id: densitySpinBox + width: base.secondColumnWidth + value: properties.density decimals: 2 - suffix: "g/cm³" + suffix: " g/cm³" stepSize: 0.01 - readOnly: !base.editingEnabled; + readOnly: !base.editingEnabled onEditingFinished: base.setMetaDataEntry("properties/density", properties.density, value) + onValueChanged: updateCostPerMeter() } Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Diameter") } ReadOnlySpinBox { - width: base.secondColumnWidth; - value: properties.diameter; + id: diameterSpinBox + width: base.secondColumnWidth + value: properties.diameter decimals: 2 - suffix: "mm" + suffix: " mm" stepSize: 0.01 - readOnly: !base.editingEnabled; + readOnly: !base.editingEnabled onEditingFinished: base.setMetaDataEntry("properties/diameter", properties.diameter, value) + onValueChanged: updateCostPerMeter() } Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Filament Cost") } SpinBox { - width: base.secondColumnWidth; - value: properties.spool_cost; - prefix: base.currency - enabled: false + id: spoolCostSpinBox + width: base.secondColumnWidth + value: base.getMaterialPreferenceValue(properties.guid, "spool_cost") + prefix: base.currency + " " + decimals: 2 + maximumValue: 1000 + + onEditingFinished: base.setMaterialPreferenceValue(properties.guid, "spool_cost", parseFloat(value)) + onValueChanged: updateCostPerMeter() } Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Filament weight") } SpinBox { - width: base.secondColumnWidth; - value: properties.spool_weight; - suffix: "g"; - stepSize: 10 - enabled: false + id: spoolWeightSpinBox + width: base.secondColumnWidth + value: base.getMaterialPreferenceValue(properties.guid, "spool_weight") + suffix: " g" + stepSize: 100 + decimals: 0 + maximumValue: 10000 + + onEditingFinished: base.setMaterialPreferenceValue(properties.guid, "spool_weight", parseFloat(value)) + onValueChanged: updateCostPerMeter() } Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Filament length") } - SpinBox + Label { - width: base.secondColumnWidth; - value: parseFloat(properties.spool_length); - suffix: "m"; - enabled: false + width: base.secondColumnWidth + text: "~ %1 m".arg(Math.round(base.spoolLength)) + verticalAlignment: Qt.AlignVCenter + height: parent.rowHeight } - Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Cost per Meter (Approx.)") } - SpinBox + Label { width: base.firstColumnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Cost per Meter") } + Label { - width: base.secondColumnWidth; - value: parseFloat(properties.cost_per_meter); - suffix: catalog.i18nc("@label", "%1/m".arg(base.currency)); - enabled: false + width: base.secondColumnWidth + text: "~ %1 %2/m".arg(base.costPerMeter.toFixed(2)).arg(base.currency) + verticalAlignment: Qt.AlignVCenter + height: parent.rowHeight } Item { width: parent.width; height: UM.Theme.getSize("default_margin").height } @@ -200,6 +220,12 @@ TabView onEditingFinished: base.setMetaDataEntry("adhesion_info", properties.adhesion_info, text) } } + + function updateCostPerMeter() + { + base.spoolLength = calculateSpoolLength(diameterSpinBox.value, densitySpinBox.value, spoolWeightSpinBox.value); + base.costPerMeter = calculateCostPerMeter(spoolCostSpinBox.value); + } } } @@ -259,6 +285,44 @@ TabView } } + function calculateSpoolLength(diameter, density, spoolWeight) + { + if(!diameter) + { + diameter = properties.diameter; + } + if(!density) + { + density = properties.density; + } + if(!spoolWeight) + { + spoolWeight = base.getMaterialPreferenceValue(properties.guid, "spool_weight"); + } + + if (diameter == 0 || density == 0 || spoolWeight == 0) + { + return 0; + } + var area = Math.PI * Math.pow(diameter / 2, 2); // in mm2 + var volume = (spoolWeight / density); // in cm3 + return volume / area; // in m + } + + function calculateCostPerMeter(spoolCost) + { + if(!spoolCost) + { + spoolCost = base.getMaterialPreferenceValue(properties.guid, "spool_cost"); + } + + if (spoolLength == 0) + { + return 0; + } + return spoolCost / spoolLength; + } + // Tiny convenience function to check if a value really changed before trying to set it. function setMetaDataEntry(entry_name, old_value, new_value) { @@ -268,6 +332,32 @@ TabView } } + function setMaterialPreferenceValue(material_guid, entry_name, new_value) + { + if(!(material_guid in materialPreferenceValues)) + { + materialPreferenceValues[material_guid] = {}; + } + if(entry_name in materialPreferenceValues[material_guid] && materialPreferenceValues[material_guid][entry_name] == new_value) + { + // value has not changed + return + } + materialPreferenceValues[material_guid][entry_name] = new_value; + + // store preference + UM.Preferences.setValue("cura/material_settings", JSON.stringify(materialPreferenceValues)); + } + + function getMaterialPreferenceValue(material_guid, entry_name) + { + if(material_guid in materialPreferenceValues && entry_name in materialPreferenceValues[material_guid]) + { + return materialPreferenceValues[material_guid][entry_name]; + } + return 0; + } + function setName(old_value, new_value) { if(old_value != new_value) diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index 264bc182e6..6072541976 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -185,17 +185,6 @@ UM.ManagementPage height: childrenRect.height Label { text: materialProperties.name; font: UM.Theme.getFont("large"); } - Button - { - id: editButton - anchors.right: parent.right; - text: catalog.i18nc("@action:button", "Edit"); - iconName: "document-edit"; - - enabled: base.currentItem != null && !base.currentItem.readOnly - - checkable: enabled - } } MaterialView @@ -209,7 +198,7 @@ UM.ManagementPage bottom: parent.bottom } - editingEnabled: editButton.checkable && editButton.checked; + editingEnabled: base.currentItem != null && !base.currentItem.readOnly properties: materialProperties containerId: base.currentItem != null ? base.currentItem.id : "" @@ -219,6 +208,7 @@ UM.ManagementPage { id: materialProperties + property string guid: "00000000-0000-0000-0000-000000000000" property string name: "Unknown"; property string profile_type: "Unknown"; property string supplier: "Unknown"; @@ -344,6 +334,7 @@ UM.ManagementPage return } materialProperties.name = currentItem.name; + materialProperties.guid = Cura.ContainerManager.getContainerMetaDataEntry(base.currentItem.id, "GUID"); if(currentItem.metadata != undefined && currentItem.metadata != null) {