From 32b0e536211ec61b77d17bfe5e062e409fbdda99 Mon Sep 17 00:00:00 2001 From: Ruben D Date: Sun, 10 Dec 2017 23:46:49 +0100 Subject: [PATCH 01/88] Add more machine information These are some additional settings that a slicer might need to know. Specifically, it needs to know these for proper X3G output. --- resources/definitions/fdmprinter.def.json | 77 +++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index ed8bf59b97..766bc22d3d 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -606,6 +606,73 @@ "settable_per_extruder": false, "settable_per_meshgroup": false }, + "machine_steps_per_mm_x": + { + "label": "Steps per Millimeter (X)", + "description": "How many steps of the stepper motor will result in one millimeter of movement in the X direction.", + "type": "int", + "default_value": 50, + "minimum_value": "0.0000001", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "machine_steps_per_mm_y": + { + "label": "Steps per Millimeter (Y)", + "description": "How many steps of the stepper motor will result in one millimeter of movement in the Y direction.", + "type": "int", + "default_value": 50, + "minimum_value": "0.0000001", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "machine_steps_per_mm_z": + { + "label": "Steps per Millimeter (Z)", + "description": "How many steps of the stepper motor will result in one millimeter of movement in the Z direction.", + "type": "int", + "default_value": 50, + "minimum_value": "0.0000001", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "machine_steps_per_mm_e": + { + "label": "Steps per Millimeter (E)", + "description": "How many steps of the stepper motors will result in one millimeter of extrusion.", + "type": "int", + "default_value": 1600, + "minimum_value": "0.0000001", + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "machine_endstop_positive_direction_x": + { + "label": "X Endstop in Positive Direction", + "description": "Whether the endstop of the X axis is in the positive direction (high X coordinate) or negative (low X coordinate).", + "type": "bool", + "default_value": false, + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "machine_endstop_positive_direction_y": + { + "label": "Y Endstop in Positive Direction", + "description": "Whether the endstop of the Y axis is in the positive direction (high Y coordinate) or negative (low Y coordinate).", + "type": "bool", + "default_value": false, + "settable_per_mesh": false, + "settable_per_extruder": true + }, + "machine_endstop_positive_direction_z": + { + "label": "Z Endstop in Positive Direction", + "description": "Whether the endstop of the Z axis is in the positive direction (high Z coordinate) or negative (low Z coordinate).", + "type": "bool", + "default_value": true, + "settable_per_mesh": false, + "settable_per_extruder": true + }, "machine_minimum_feedrate": { "label": "Minimum Feedrate", @@ -616,6 +683,16 @@ "settable_per_mesh": false, "settable_per_extruder": false, "settable_per_meshgroup": false + }, + "machine_feeder_wheel_diameter": + { + "label": "Feeder Wheel Diameter", + "description": "The diameter of the wheel that drives the material in the feeder.", + "unit": "mm", + "type": "float", + "default_value": 10.0, + "settable_per_mesh": false, + "settable_per_extruder": true } } }, From 9917d567a333166019dc08c778331a93c571770e Mon Sep 17 00:00:00 2001 From: Ruben D Date: Sun, 10 Dec 2017 23:48:11 +0100 Subject: [PATCH 02/88] Correction to M180 settings for X3G These settings are needed for X3G output to have the correct dimensions and feedrate. The steps_per_mm settings are important. --- resources/definitions/m180.def.json | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/resources/definitions/m180.def.json b/resources/definitions/m180.def.json index 1e8ac1767b..1fab24f702 100644 --- a/resources/definitions/m180.def.json +++ b/resources/definitions/m180.def.json @@ -25,8 +25,7 @@ "default_value": true }, "machine_nozzle_size": { - "default_value": 0.4, - "minimum_value": "0.001" + "default_value": 0.4 }, "machine_head_with_fans_polygon": { "default_value": [ @@ -36,6 +35,21 @@ [ 18, 35 ] ] }, + "machine_max_feedrate_z": { + "default_value": 400 + }, + "steps_per_mm_x": { + "default_value": 93 + }, + "steps_per_mm_y": { + "default_value": 93 + }, + "steps_per_mm_z": { + "default_value": 1600 + }, + "steps_per_mm_e": { + "default_value": 92 + }, "gantry_height": { "default_value": 55 }, From c4aae784ee3c5befd5ab76b3c947e8096c1ae59e Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 10 Jan 2018 20:53:58 +0100 Subject: [PATCH 03/88] Improve tooltip text quality --- resources/qml/PrinterOutput/ExtruderBox.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml index a7141262a9..4e15ba75c5 100644 --- a/resources/qml/PrinterOutput/ExtruderBox.qml +++ b/resources/qml/PrinterOutput/ExtruderBox.qml @@ -52,7 +52,7 @@ Item { base.showTooltip( base, - {x: 0, y: extruderTargetTemperature.mapToItem(base, 0, -parent.height / 4).y}, + {x: 0, y: extruderTargetTemperature.mapToItem(base, 0, Math.floor(-parent.height / 4)).y}, catalog.i18nc("@tooltip", "The target temperature of the hotend. The hotend will heat up or cool down towards this temperature. If this is 0, the hotend heating is turned off.") ); } @@ -85,7 +85,7 @@ Item { base.showTooltip( base, - {x: 0, y: parent.mapToItem(base, 0, -parent.height / 4).y}, + {x: 0, y: parent.mapToItem(base, 0, Math.floor(-parent.height / 4)).y}, catalog.i18nc("@tooltip", "The current temperature of this extruder.") ); } From fb9d841c90e4b0d685bcba570da1d0b2495a77ce Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 10 Jan 2018 21:41:59 +0100 Subject: [PATCH 04/88] Add per-extruder preheat controls These are lifted from the bed preheat controls and are not functional at the moment --- cura/PrinterOutput/PrinterOutputController.py | 1 + cura/PrinterOutput/PrinterOutputModel.py | 7 + .../ClusterUM3PrinterOutputController.py | 1 + resources/qml/PrinterOutput/ExtruderBox.qml | 258 +++++++++++++++++- 4 files changed, 264 insertions(+), 3 deletions(-) diff --git a/cura/PrinterOutput/PrinterOutputController.py b/cura/PrinterOutput/PrinterOutputController.py index 86ca10e2d3..6bfe562f1a 100644 --- a/cura/PrinterOutput/PrinterOutputController.py +++ b/cura/PrinterOutput/PrinterOutputController.py @@ -15,6 +15,7 @@ class PrinterOutputController: self.can_pause = True self.can_abort = True self.can_pre_heat_bed = True + self.can_pre_heat_extruders = True self.can_control_manually = True self._output_device = output_device diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 8234989519..cbed44fa65 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -218,6 +218,13 @@ class PrinterOutputModel(QObject): return self._controller.can_pre_heat_bed return False + # Does the printer support pre-heating the bed at all + @pyqtProperty(bool, constant=True) + def canPreHeatExtruders(self): + if self._controller: + return self._controller.can_pre_heat_extruders + return False + # Does the printer support pause at all @pyqtProperty(bool, constant=True) def canPause(self): diff --git a/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py index 4615cd62dc..8909a15d2c 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py @@ -13,6 +13,7 @@ class ClusterUM3PrinterOutputController(PrinterOutputController): def __init__(self, output_device): super().__init__(output_device) self.can_pre_heat_bed = False + self.can_pre_heat_extruders = False self.can_control_manually = False def setJobState(self, job: "PrintJobOutputModel", state: str): diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml index 4e15ba75c5..609bd65c1d 100644 --- a/resources/qml/PrinterOutput/ExtruderBox.qml +++ b/resources/qml/PrinterOutput/ExtruderBox.qml @@ -39,7 +39,7 @@ Item color: UM.Theme.getColor("text_inactive") anchors.right: parent.right anchors.rightMargin: UM.Theme.getSize("default_margin").width - anchors.bottom: extruderTemperature.bottom + anchors.bottom: extruderCurrentTemperature.bottom MouseArea //For tooltip. { @@ -65,7 +65,7 @@ Item } Label //Temperature indication. { - id: extruderTemperature + id: extruderCurrentTemperature text: Math.round(extruderModel.hotendTemperature) + "°C" //text: (connectedPrinter != null && connectedPrinter.hotendIds[index] != null && connectedPrinter.hotendTemperatures[index] != null) ? Math.round(connectedPrinter.hotendTemperatures[index]) + "°C" : "" color: UM.Theme.getColor("text") @@ -76,7 +76,7 @@ Item MouseArea //For tooltip. { - id: extruderTemperatureTooltipArea + id: extruderCurrentTemperatureTooltipArea hoverEnabled: true anchors.fill: parent onHoveredChanged: @@ -97,6 +97,258 @@ Item } } + Rectangle //Input field for pre-heat temperature. + { + id: preheatTemperatureControl + color: !enabled ? UM.Theme.getColor("setting_control_disabled") : showError ? UM.Theme.getColor("setting_validation_error_background") : UM.Theme.getColor("setting_validation_ok") + property var showError: + { + if(extruderTemperature.properties.maximum_value != "None" && extruderTemperature.properties.maximum_value < Math.floor(preheatTemperatureInput.text)) + { + return true; + } else + { + return false; + } + } + enabled: + { + if (extruderModel == null) + { + return false; //Can't preheat if not connected. + } + if (!connectedPrinter.acceptsCommands) + { + return false; //Not allowed to do anything. + } + if (connectedPrinter.jobState == "printing" || connectedPrinter.jobState == "pre_print" || connectedPrinter.jobState == "resuming" || connectedPrinter.jobState == "pausing" || connectedPrinter.jobState == "paused" || connectedPrinter.jobState == "error" || connectedPrinter.jobState == "offline") + { + return false; //Printer is in a state where it can't react to pre-heating. + } + return true; + } + border.width: UM.Theme.getSize("default_lining").width + border.color: !enabled ? UM.Theme.getColor("setting_control_disabled_border") : preheatTemperatureInputMouseArea.containsMouse ? UM.Theme.getColor("setting_control_border_highlight") : UM.Theme.getColor("setting_control_border") + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.bottom: parent.bottom + anchors.bottomMargin: UM.Theme.getSize("default_margin").height + width: Math.floor(UM.Theme.getSize("setting_control").width * 0.75) + height: UM.Theme.getSize("setting_control").height + visible: extruderModel != null ? extruderModel.canPreHeatExtruders: true + Rectangle //Highlight of input field. + { + anchors.fill: parent + anchors.margins: UM.Theme.getSize("default_lining").width + color: UM.Theme.getColor("setting_control_highlight") + opacity: preheatTemperatureControl.hovered ? 1.0 : 0 + } + MouseArea //Change cursor on hovering. + { + id: preheatTemperatureInputMouseArea + hoverEnabled: true + anchors.fill: parent + cursorShape: Qt.IBeamCursor + + onHoveredChanged: + { + if (containsMouse) + { + base.showTooltip( + base, + {x: 0, y: preheatTemperatureInputMouseArea.mapToItem(base, 0, 0).y}, + catalog.i18nc("@tooltip of temperature input", "The temperature to pre-heat the extruder to.") + ); + } + else + { + base.hideTooltip(); + } + } + } + TextInput + { + id: preheatTemperatureInput + font: UM.Theme.getFont("default") + color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text") + selectByMouse: true + maximumLength: 5 + enabled: parent.enabled + validator: RegExpValidator { regExp: /^-?[0-9]{0,9}[.,]?[0-9]{0,10}$/ } //Floating point regex. + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("setting_unit_margin").width + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + renderType: Text.NativeRendering + + Component.onCompleted: + { + if (!extruderTemperature.properties.value) + { + text = ""; + } + else + { + text = extruderTemperature.properties.value; + } + } + } + } + + Button //The pre-heat button. + { + id: preheatButton + height: UM.Theme.getSize("setting_control").height + visible: extruderModel != null ? extruderModel.canPreHeatExtruders: true + enabled: + { + if (!preheatTemperatureControl.enabled) + { + return false; //Not connected, not authenticated or printer is busy. + } + if (extruderModel.isPreheating) + { + return true; + } + if (extruderTemperature.properties.minimum_value != "None" && Math.floor(preheatTemperatureInput.text) < Math.floor(extruderTemperature.properties.minimum_value)) + { + return false; //Target temperature too low. + } + if (extruderTemperature.properties.maximum_value != "None" && Math.floor(preheatTemperatureInput.text) > Math.floor(extruderTemperature.properties.maximum_value)) + { + return false; //Target temperature too high. + } + if (Math.floor(preheatTemperatureInput.text) == 0) + { + return false; //Setting the temperature to 0 is not allowed (since that cancels the pre-heating). + } + return true; //Preconditions are met. + } + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: UM.Theme.getSize("default_margin").width + style: ButtonStyle { + background: Rectangle + { + border.width: UM.Theme.getSize("default_lining").width + implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("default_margin").width * 2) + border.color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled_border"); + } + else if(control.pressed) + { + return UM.Theme.getColor("action_button_active_border"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered_border"); + } + else + { + return UM.Theme.getColor("action_button_border"); + } + } + color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled"); + } + else if(control.pressed) + { + return UM.Theme.getColor("action_button_active"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered"); + } + else + { + return UM.Theme.getColor("action_button"); + } + } + Behavior on color + { + ColorAnimation + { + duration: 50 + } + } + + Label + { + id: actualLabel + anchors.centerIn: parent + color: + { + if(!control.enabled) + { + return UM.Theme.getColor("action_button_disabled_text"); + } + else if(control.pressed) + { + return UM.Theme.getColor("action_button_active_text"); + } + else if(control.hovered) + { + return UM.Theme.getColor("action_button_hovered_text"); + } + else + { + return UM.Theme.getColor("action_button_text"); + } + } + font: UM.Theme.getFont("action_button") + text: + { + if(extruderModel == null) + { + return "" + } + if(extruderModel.isPreheating ) + { + return catalog.i18nc("@button Cancel pre-heating", "Cancel") + } else + { + return catalog.i18nc("@button", "Pre-heat") + } + } + } + } + } + + onClicked: + { + if (!extruderModel.isPreheating) + { + extruderModel.preheatExtruder(preheatTemperatureInput.text, 900); + } + else + { + extruderModel.cancelPreheatExtruder(); + } + } + + onHoveredChanged: + { + if (hovered) + { + base.showTooltip( + base, + {x: 0, y: preheatButton.mapToItem(base, 0, 0).y}, + catalog.i18nc("@tooltip of pre-heat", "Heat the extruder in advance before printing. You can continue adjusting your print while it is heating, and you won't have to wait for the extruder to heat up when you're ready to print.") + ); + } + else + { + base.hideTooltip(); + } + } + } + Rectangle //Material colour indication. { id: materialColor From 52412cb1cdf328acd8c9e14c150063df5c98060b Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 10 Jan 2018 21:59:17 +0100 Subject: [PATCH 05/88] Remove commented-out code --- resources/qml/PrinterOutput/ExtruderBox.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml index 609bd65c1d..80f2b94f6c 100644 --- a/resources/qml/PrinterOutput/ExtruderBox.qml +++ b/resources/qml/PrinterOutput/ExtruderBox.qml @@ -34,7 +34,6 @@ Item { id: extruderTargetTemperature text: Math.round(extruderModel.targetHotendTemperature) + "°C" - //text: (connectedPrinter != null && connectedPrinter.hotendIds[index] != null && connectedPrinter.targetHotendTemperatures[index] != null) ? Math.round(connectedPrinter.targetHotendTemperatures[index]) + "°C" : "" font: UM.Theme.getFont("small") color: UM.Theme.getColor("text_inactive") anchors.right: parent.right @@ -67,7 +66,6 @@ Item { id: extruderCurrentTemperature text: Math.round(extruderModel.hotendTemperature) + "°C" - //text: (connectedPrinter != null && connectedPrinter.hotendIds[index] != null && connectedPrinter.hotendTemperatures[index] != null) ? Math.round(connectedPrinter.hotendTemperatures[index]) + "°C" : "" color: UM.Theme.getColor("text") font: UM.Theme.getFont("large") anchors.right: extruderTargetTemperature.left From d3f94a1137cea7352bdb2ec3ef60b09a7ff80f2f Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 10 Jan 2018 22:36:52 +0100 Subject: [PATCH 06/88] Get preheat temperatures from the proper stacks --- resources/qml/PrinterOutput/ExtruderBox.qml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml index 80f2b94f6c..f1d4a61604 100644 --- a/resources/qml/PrinterOutput/ExtruderBox.qml +++ b/resources/qml/PrinterOutput/ExtruderBox.qml @@ -15,6 +15,18 @@ Item //width: index == machineExtruderCount.properties.value - 1 && index % 2 == 0 ? extrudersGrid.width : Math.floor(extrudersGrid.width / 2 - UM.Theme.getSize("sidebar_lining_thin").width / 2) implicitWidth: parent.width implicitHeight: UM.Theme.getSize("sidebar_extruder_box").height + + UM.SettingPropertyProvider + { + id: extruderTemperature + containerStackId: Cura.ExtruderManager.extruderIds[position] + key: "material_print_temperature" + watchedProperties: ["value", "minimum_value", "maximum_value", "resolve"] + storeIndex: 0 + + property var resolve: Cura.MachineManager.activeStackId != Cura.MachineManager.activeMachineId ? properties.resolve : "None" + } + Rectangle { id: background From 70cd6aad534ac43d6def756ee250dc60086de13f Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 11 Jan 2018 15:17:30 +0100 Subject: [PATCH 07/88] Remove commented-out code --- resources/qml/PrinterOutput/ExtruderBox.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml index f1d4a61604..171fbf2013 100644 --- a/resources/qml/PrinterOutput/ExtruderBox.qml +++ b/resources/qml/PrinterOutput/ExtruderBox.qml @@ -12,7 +12,6 @@ Item property alias color: background.color property var extruderModel property var position: index - //width: index == machineExtruderCount.properties.value - 1 && index % 2 == 0 ? extrudersGrid.width : Math.floor(extrudersGrid.width / 2 - UM.Theme.getSize("sidebar_lining_thin").width / 2) implicitWidth: parent.width implicitHeight: UM.Theme.getSize("sidebar_extruder_box").height From bc5b5ac283765078b25c157e6ec64738169afdf9 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 11 Jan 2018 15:27:38 +0100 Subject: [PATCH 08/88] Implement preheating hotends for USB printing --- cura/PrinterOutput/ExtruderOuputModel.py | 33 ++++++++++++- cura/PrinterOutput/PrinterOutputController.py | 2 +- cura/PrinterOutput/PrinterOutputModel.py | 6 +-- .../ClusterUM3PrinterOutputController.py | 2 +- .../USBPrinting/USBPrinterOutputController.py | 49 +++++++++++++++++-- resources/qml/PrinterOutput/ExtruderBox.qml | 14 +++--- 6 files changed, 89 insertions(+), 17 deletions(-) diff --git a/cura/PrinterOutput/ExtruderOuputModel.py b/cura/PrinterOutput/ExtruderOuputModel.py index b0be6cbbe4..befde28f99 100644 --- a/cura/PrinterOutput/ExtruderOuputModel.py +++ b/cura/PrinterOutput/ExtruderOuputModel.py @@ -17,14 +17,23 @@ class ExtruderOutputModel(QObject): targetHotendTemperatureChanged = pyqtSignal() hotendTemperatureChanged = pyqtSignal() activeMaterialChanged = pyqtSignal() + isPreheatingChanged = pyqtSignal() - def __init__(self, printer: "PrinterOutputModel", parent=None): + def __init__(self, printer: "PrinterOutputModel", position: int, parent=None): super().__init__(parent) self._printer = printer + self._position = position self._target_hotend_temperature = 0 self._hotend_temperature = 0 self._hotend_id = "" self._active_material = None # type: Optional[MaterialOutputModel] + self._is_preheating = False + + def getPrinter(self): + return self._printer + + def getPosition(self): + return self._position @pyqtProperty(QObject, notify = activeMaterialChanged) def activeMaterial(self) -> "MaterialOutputModel": @@ -68,3 +77,25 @@ class ExtruderOutputModel(QObject): if self._hotend_id != id: self._hotend_id = id self.hotendIDChanged.emit() + + def updateIsPreheating(self, pre_heating): + if self._is_preheating != pre_heating: + self._is_preheating = pre_heating + self.isPreheatingChanged.emit() + + @pyqtProperty(bool, notify=isPreheatingChanged) + def isPreheating(self): + return self._is_preheating + + ## Pre-heats the extruder before printer. + # + # \param temperature The temperature to heat the extruder to, in degrees + # Celsius. + # \param duration How long the bed should stay warm, in seconds. + @pyqtSlot(float, float) + def preheatHotend(self, temperature, duration): + self._printer._controller.preheatHotend(self, temperature, duration) + + @pyqtSlot() + def cancelPreheatHotend(self): + self._printer._controller.cancelPreheatHotend(self) \ No newline at end of file diff --git a/cura/PrinterOutput/PrinterOutputController.py b/cura/PrinterOutput/PrinterOutputController.py index 6bfe562f1a..512ac4aa02 100644 --- a/cura/PrinterOutput/PrinterOutputController.py +++ b/cura/PrinterOutput/PrinterOutputController.py @@ -15,7 +15,7 @@ class PrinterOutputController: self.can_pause = True self.can_abort = True self.can_pre_heat_bed = True - self.can_pre_heat_extruders = True + self.can_pre_heat_hotends = True self.can_control_manually = True self._output_device = output_device diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index cbed44fa65..f510829101 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -32,7 +32,7 @@ class PrinterOutputModel(QObject): self._name = "" self._key = "" # Unique identifier self._controller = output_controller - self._extruders = [ExtruderOutputModel(printer=self) for i in range(number_of_extruders)] + self._extruders = [ExtruderOutputModel(printer=self, position=i) for i in range(number_of_extruders)] self._head_position = Vector(0, 0, 0) self._active_print_job = None # type: Optional[PrintJobOutputModel] self._firmware_version = firmware_version @@ -220,9 +220,9 @@ class PrinterOutputModel(QObject): # Does the printer support pre-heating the bed at all @pyqtProperty(bool, constant=True) - def canPreHeatExtruders(self): + def canPreHeatHotends(self): if self._controller: - return self._controller.can_pre_heat_extruders + return self._controller.can_pre_heat_hotends return False # Does the printer support pause at all diff --git a/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py index 8909a15d2c..076c4584af 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3PrinterOutputController.py @@ -13,7 +13,7 @@ class ClusterUM3PrinterOutputController(PrinterOutputController): def __init__(self, output_device): super().__init__(output_device) self.can_pre_heat_bed = False - self.can_pre_heat_extruders = False + self.can_pre_heat_hotends = False self.can_control_manually = False def setJobState(self, job: "PrintJobOutputModel", state: str): diff --git a/plugins/USBPrinting/USBPrinterOutputController.py b/plugins/USBPrinting/USBPrinterOutputController.py index ba45e7b0ca..5640db4a63 100644 --- a/plugins/USBPrinting/USBPrinterOutputController.py +++ b/plugins/USBPrinting/USBPrinterOutputController.py @@ -19,6 +19,11 @@ class USBPrinterOuptutController(PrinterOutputController): self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished) self._preheat_printer = None + self._preheat_extruders_timer = QTimer() + self._preheat_extruders_timer.setSingleShot(True) + self._preheat_extruders_timer.timeout.connect(self._onPreheatHotendsTimerFinished) + self._preheat_extruders = set() + def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): self._output_device.sendCommand("G91") self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) @@ -42,6 +47,9 @@ class USBPrinterOuptutController(PrinterOutputController): self._output_device.cancelPrint() pass + def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): + self._output_device.sendCommand("M140 S%s" % temperature) + def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): try: temperature = round(temperature) # The API doesn't allow floating point. @@ -60,9 +68,42 @@ class USBPrinterOuptutController(PrinterOutputController): self._preheat_bed_timer.stop() printer.updateIsPreheating(False) - def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): - self._output_device.sendCommand("M140 S%s" % temperature) - def _onPreheatBedTimerFinished(self): self.setTargetBedTemperature(self._preheat_printer, 0) - self._preheat_printer.updateIsPreheating(False) \ No newline at end of file + self._preheat_printer.updateIsPreheating(False) + + def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: int): + self._output_device.sendCommand("M104 S%s T%s" % (temperature, position)) + + def _onPreheatHotendsTimerFinished(self): + for extruder in self._preheat_extruders: + self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0) + self._preheat_extruders = set() + self._preheat_printer.updateIsPreheating(False) + + def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"): + self.preheatHotend(extruder, temperature=0, duration=0) + self._preheat_extruders_timer.stop() + try: + self._preheat_extruders.remove(extruder) + except KeyError: + pass + extruder.updateIsPreheating(False) + + def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration): + position = extruder.getPosition() + number_of_extruders = len(extruder.getPrinter().extruders) + if position >= number_of_extruders: + return # Got invalid extruder nr, can't pre-heat. + + try: + temperature = round(temperature) # The API doesn't allow floating point. + duration = round(duration) + except ValueError: + return # Got invalid values, can't pre-heat. + + self.setTargetHotendTemperature(extruder.getPrinter(), position, temperature=temperature) + self._preheat_extruders_timer.setInterval(duration * 1000) + self._preheat_extruders_timer.start() + self._preheat_extruders.add(extruder) + extruder.updateIsPreheating(True) \ No newline at end of file diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml index 171fbf2013..dee9377425 100644 --- a/resources/qml/PrinterOutput/ExtruderBox.qml +++ b/resources/qml/PrinterOutput/ExtruderBox.qml @@ -95,7 +95,7 @@ Item base.showTooltip( base, {x: 0, y: parent.mapToItem(base, 0, Math.floor(-parent.height / 4)).y}, - catalog.i18nc("@tooltip", "The current temperature of this extruder.") + catalog.i18nc("@tooltip", "The current temperature of this hotend.") ); } else @@ -144,7 +144,7 @@ Item anchors.bottomMargin: UM.Theme.getSize("default_margin").height width: Math.floor(UM.Theme.getSize("setting_control").width * 0.75) height: UM.Theme.getSize("setting_control").height - visible: extruderModel != null ? extruderModel.canPreHeatExtruders: true + visible: extruderModel != null ? extruderModel.canPreHeatHotends: true Rectangle //Highlight of input field. { anchors.fill: parent @@ -166,7 +166,7 @@ Item base.showTooltip( base, {x: 0, y: preheatTemperatureInputMouseArea.mapToItem(base, 0, 0).y}, - catalog.i18nc("@tooltip of temperature input", "The temperature to pre-heat the extruder to.") + catalog.i18nc("@tooltip of temperature input", "The temperature to pre-heat the hotend to.") ); } else @@ -208,7 +208,7 @@ Item { id: preheatButton height: UM.Theme.getSize("setting_control").height - visible: extruderModel != null ? extruderModel.canPreHeatExtruders: true + visible: extruderModel != null ? extruderModel.canPreHeatHotends: true enabled: { if (!preheatTemperatureControl.enabled) @@ -333,11 +333,11 @@ Item { if (!extruderModel.isPreheating) { - extruderModel.preheatExtruder(preheatTemperatureInput.text, 900); + extruderModel.preheatHotend(preheatTemperatureInput.text, 900); } else { - extruderModel.cancelPreheatExtruder(); + extruderModel.cancelPreheatHotend(); } } @@ -348,7 +348,7 @@ Item base.showTooltip( base, {x: 0, y: preheatButton.mapToItem(base, 0, 0).y}, - catalog.i18nc("@tooltip of pre-heat", "Heat the extruder in advance before printing. You can continue adjusting your print while it is heating, and you won't have to wait for the extruder to heat up when you're ready to print.") + catalog.i18nc("@tooltip of pre-heat", "Heat the hotend in advance before printing. You can continue adjusting your print while it is heating, and you won't have to wait for the hotend to heat up when you're ready to print.") ); } else From 7994c95bbe4cf9f6928b3e4baf811e3705eb63cf Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 17 Jan 2018 18:03:01 +0100 Subject: [PATCH 09/88] Add unit to preheat temperature boxes --- resources/qml/PrinterOutput/ExtruderBox.qml | 23 +++++++++++++----- resources/qml/PrinterOutput/HeatedBedBox.qml | 25 ++++++++++++++------ resources/themes/cura-light/theme.json | 2 ++ 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml index dee9377425..4aacef1a94 100644 --- a/resources/qml/PrinterOutput/ExtruderBox.qml +++ b/resources/qml/PrinterOutput/ExtruderBox.qml @@ -138,13 +138,13 @@ Item } border.width: UM.Theme.getSize("default_lining").width border.color: !enabled ? UM.Theme.getColor("setting_control_disabled_border") : preheatTemperatureInputMouseArea.containsMouse ? UM.Theme.getColor("setting_control_border_highlight") : UM.Theme.getColor("setting_control_border") - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: preheatButton.left + anchors.rightMargin: UM.Theme.getSize("default_margin").width anchors.bottom: parent.bottom anchors.bottomMargin: UM.Theme.getSize("default_margin").height - width: Math.floor(UM.Theme.getSize("setting_control").width * 0.75) - height: UM.Theme.getSize("setting_control").height - visible: extruderModel != null ? extruderModel.canPreHeatHotends: true + width: UM.Theme.getSize("monitor_preheat_temperature_control").width + height: UM.Theme.getSize("monitor_preheat_temperature_control").height + visible: extruderModel != null ? extruderModel.canPreHeatHotends && !extruderModel.isPreheating : true Rectangle //Highlight of input field. { anchors.fill: parent @@ -175,6 +175,17 @@ Item } } } + Label + { + id: unit + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("setting_unit_margin").width + anchors.verticalCenter: parent.verticalCenter + + text: "°C"; + color: UM.Theme.getColor("setting_unit") + font: UM.Theme.getFont("default") + } TextInput { id: preheatTemperatureInput @@ -186,7 +197,7 @@ Item validator: RegExpValidator { regExp: /^-?[0-9]{0,9}[.,]?[0-9]{0,10}$/ } //Floating point regex. anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("setting_unit_margin").width - anchors.right: parent.right + anchors.right: unit.left anchors.verticalCenter: parent.verticalCenter renderType: Text.NativeRendering diff --git a/resources/qml/PrinterOutput/HeatedBedBox.qml b/resources/qml/PrinterOutput/HeatedBedBox.qml index bc89da2251..8d1e37b23d 100644 --- a/resources/qml/PrinterOutput/HeatedBedBox.qml +++ b/resources/qml/PrinterOutput/HeatedBedBox.qml @@ -122,13 +122,13 @@ Item } border.width: UM.Theme.getSize("default_lining").width border.color: !enabled ? UM.Theme.getColor("setting_control_disabled_border") : preheatTemperatureInputMouseArea.containsMouse ? UM.Theme.getColor("setting_control_border_highlight") : UM.Theme.getColor("setting_control_border") - anchors.left: parent.left - anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: preheatButton.left + anchors.rightMargin: UM.Theme.getSize("default_margin").width anchors.bottom: parent.bottom anchors.bottomMargin: UM.Theme.getSize("default_margin").height - width: UM.Theme.getSize("setting_control").width - height: UM.Theme.getSize("setting_control").height - visible: printerModel != null ? printerModel.canPreHeatBed: true + width: UM.Theme.getSize("monitor_preheat_temperature_control").width + height: UM.Theme.getSize("monitor_preheat_temperature_control").height + visible: printerModel != null ? printerModel.canPreHeatBed && !printerModel.isPreheating : true Rectangle //Highlight of input field. { anchors.fill: parent @@ -159,18 +159,29 @@ Item } } } + Label + { + id: unit + anchors.right: parent.right + anchors.rightMargin: UM.Theme.getSize("setting_unit_margin").width + anchors.verticalCenter: parent.verticalCenter + + text: "°C"; + color: UM.Theme.getColor("setting_unit") + font: UM.Theme.getFont("default") + } TextInput { id: preheatTemperatureInput font: UM.Theme.getFont("default") color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text") selectByMouse: true - maximumLength: 10 + maximumLength: 5 enabled: parent.enabled validator: RegExpValidator { regExp: /^-?[0-9]{0,9}[.,]?[0-9]{0,10}$/ } //Floating point regex. anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("setting_unit_margin").width - anchors.right: parent.right + anchors.right: unit.left anchors.verticalCenter: parent.verticalCenter renderType: Text.NativeRendering diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 51c96a5f82..aebd527749 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -379,6 +379,8 @@ "save_button_save_to_button": [0.3, 2.7], "save_button_specs_icons": [1.4, 1.4], + "monitor_preheat_temperature_control": [4.5, 2.0], + "modal_window_minimum": [60.0, 45], "license_window_minimum": [45, 45], "wizard_progress": [10.0, 0.0], From b68a30662aead1418c931d7c58ec2d2ab24f25f5 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Wed, 17 Jan 2018 21:21:13 +0100 Subject: [PATCH 10/88] Add stubs to PrinterOutputController --- cura/PrinterOutput/PrinterOutputController.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cura/PrinterOutput/PrinterOutputController.py b/cura/PrinterOutput/PrinterOutputController.py index 512ac4aa02..6710216a02 100644 --- a/cura/PrinterOutput/PrinterOutputController.py +++ b/cura/PrinterOutput/PrinterOutputController.py @@ -34,6 +34,12 @@ class PrinterOutputController: def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): Logger.log("w", "Preheat bed not implemented in controller") + def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"): + Logger.log("w", "Cancel preheat hotend not implemented in controller") + + def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration): + Logger.log("w", "Preheat hotend not implemented in controller") + def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed): Logger.log("w", "Set head position not implemented in controller") From 0f0b1c6795374b7434ccf994bf554e8c88840b6e Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 18 Jan 2018 00:05:16 +0100 Subject: [PATCH 11/88] Fix getting ability to preheat from extruder --- cura/PrinterOutput/ExtruderOuputModel.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cura/PrinterOutput/ExtruderOuputModel.py b/cura/PrinterOutput/ExtruderOuputModel.py index befde28f99..d2fbabea8f 100644 --- a/cura/PrinterOutput/ExtruderOuputModel.py +++ b/cura/PrinterOutput/ExtruderOuputModel.py @@ -35,6 +35,13 @@ class ExtruderOutputModel(QObject): def getPosition(self): return self._position + # Does the printer support pre-heating the bed at all + @pyqtProperty(bool, constant=True) + def canPreHeatHotends(self): + if self._printer: + return self._printer.canPreHeatHotends + return False + @pyqtProperty(QObject, notify = activeMaterialChanged) def activeMaterial(self) -> "MaterialOutputModel": return self._active_material From 3fed44bb5e84d74fe55d53af07f180cecfc707ce Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 18 Jan 2018 00:42:41 +0100 Subject: [PATCH 12/88] Fix typo in class name --- plugins/USBPrinting/USBPrinterOutputController.py | 2 +- plugins/USBPrinting/USBPrinterOutputDevice.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/USBPrinting/USBPrinterOutputController.py b/plugins/USBPrinting/USBPrinterOutputController.py index 5640db4a63..ec47878f9c 100644 --- a/plugins/USBPrinting/USBPrinterOutputController.py +++ b/plugins/USBPrinting/USBPrinterOutputController.py @@ -10,7 +10,7 @@ if MYPY: from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel -class USBPrinterOuptutController(PrinterOutputController): +class USBPrinterOutputController(PrinterOutputController): def __init__(self, output_device): super().__init__(output_device) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index d372b54c38..b53f502d81 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -12,7 +12,7 @@ from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from .AutoDetectBaudJob import AutoDetectBaudJob -from .USBPrinterOutputController import USBPrinterOuptutController +from .USBPrinterOutputController import USBPrinterOutputController from .avr_isp import stk500v2, intelHex from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty @@ -237,7 +237,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): container_stack = Application.getInstance().getGlobalContainerStack() num_extruders = container_stack.getProperty("machine_extruder_count", "value") # Ensure that a printer is created. - self._printers = [PrinterOutputModel(output_controller=USBPrinterOuptutController(self), number_of_extruders=num_extruders)] + self._printers = [PrinterOutputModel(output_controller=USBPrinterOutputController(self), number_of_extruders=num_extruders)] self._printers[0].updateName(container_stack.getName()) self.setConnectionState(ConnectionState.connected) self._update_thread.start() @@ -364,7 +364,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): elapsed_time = int(time() - self._print_start_time) print_job = self._printers[0].activePrintJob if print_job is None: - print_job = PrintJobOutputModel(output_controller = USBPrinterOuptutController(self), name= Application.getInstance().getPrintInformation().jobName) + print_job = PrintJobOutputModel(output_controller = USBPrinterOutputController(self), name= Application.getInstance().getPrintInformation().jobName) print_job.updateState("printing") self._printers[0].updateActivePrintJob(print_job) From 13206e1fdcc80ae05a2c30902ebd668373101710 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 18 Jan 2018 00:46:20 +0100 Subject: [PATCH 13/88] Stop preheating when a print is started or the temperature is set to 0 on the printer --- .../USBPrinting/USBPrinterOutputController.py | 91 ++++++++++++++----- 1 file changed, 68 insertions(+), 23 deletions(-) diff --git a/plugins/USBPrinting/USBPrinterOutputController.py b/plugins/USBPrinting/USBPrinterOutputController.py index ec47878f9c..b59e60c244 100644 --- a/plugins/USBPrinting/USBPrinterOutputController.py +++ b/plugins/USBPrinting/USBPrinterOutputController.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 cura.PrinterOutput.PrinterOutputController import PrinterOutputController @@ -19,10 +19,39 @@ class USBPrinterOutputController(PrinterOutputController): self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished) self._preheat_printer = None - self._preheat_extruders_timer = QTimer() - self._preheat_extruders_timer.setSingleShot(True) - self._preheat_extruders_timer.timeout.connect(self._onPreheatHotendsTimerFinished) - self._preheat_extruders = set() + self._preheat_hotends_timer = QTimer() + self._preheat_hotends_timer.setSingleShot(True) + self._preheat_hotends_timer.timeout.connect(self._onPreheatHotendsTimerFinished) + self._preheat_hotends = set() + + self._output_device.printersChanged.connect(self._onPrintersChanged) + self._active_printer = None + + def _onPrintersChanged(self): + if self._active_printer: + self._active_printer.stateChanged.disconnect(self._onPrinterStateChanged) + self._active_printer.targetBedTemperatureChanged.disconnect(self._onTargetBedTemperatureChanged) + for extruder in self._active_printer.extruders: + extruder.targetHotendTemperatureChanged.disconnect(self._onTargetHotendTemperatureChanged) + + self._active_printer = self._output_device.activePrinter + if self._active_printer: + self._active_printer.stateChanged.connect(self._onPrinterStateChanged) + self._active_printer.targetBedTemperatureChanged.connect(self._onTargetBedTemperatureChanged) + for extruder in self._active_printer.extruders: + extruder.targetHotendTemperatureChanged.connect(self._onTargetHotendTemperatureChanged) + + def _onPrinterStateChanged(self): + if self._active_printer.state != "idle": + if self._preheat_bed_timer.isActive(): + self._preheat_bed_timer.stop() + self._preheat_printer.updateIsPreheating(False) + if self._preheat_hotends_timer.isActive(): + self._preheat_hotends_timer.stop() + for extruder in self._preheat_hotends: + extruder.updateIsPreheating(False) + self._preheat_hotends = set() + def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): self._output_device.sendCommand("G91") @@ -47,9 +76,15 @@ class USBPrinterOutputController(PrinterOutputController): self._output_device.cancelPrint() pass + def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): self._output_device.sendCommand("M140 S%s" % temperature) + def _onTargetBedTemperatureChanged(self): + if self._preheat_bed_timer.isActive() and self._preheat_printer.targetBedTemperature == 0: + self._preheat_bed_timer.stop() + self._preheat_printer.updateIsPreheating(False) + def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): try: temperature = round(temperature) # The API doesn't allow floating point. @@ -64,7 +99,7 @@ class USBPrinterOutputController(PrinterOutputController): printer.updateIsPreheating(True) def cancelPreheatBed(self, printer: "PrinterOutputModel"): - self.preheatBed(printer, temperature=0, duration=0) + self.setTargetBedTemperature(printer, temperature=0) self._preheat_bed_timer.stop() printer.updateIsPreheating(False) @@ -72,23 +107,20 @@ class USBPrinterOutputController(PrinterOutputController): self.setTargetBedTemperature(self._preheat_printer, 0) self._preheat_printer.updateIsPreheating(False) + def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: int): self._output_device.sendCommand("M104 S%s T%s" % (temperature, position)) - def _onPreheatHotendsTimerFinished(self): - for extruder in self._preheat_extruders: - self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0) - self._preheat_extruders = set() - self._preheat_printer.updateIsPreheating(False) + def _onTargetHotendTemperatureChanged(self): + if not self._preheat_hotends_timer.isActive(): + return - def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"): - self.preheatHotend(extruder, temperature=0, duration=0) - self._preheat_extruders_timer.stop() - try: - self._preheat_extruders.remove(extruder) - except KeyError: - pass - extruder.updateIsPreheating(False) + for extruder in self._active_printer.extruders: + if extruder in self._preheat_hotends and extruder.targetHotendTemperature == 0: + extruder.updateIsPreheating(False) + self._preheat_hotends.remove(extruder) + if not self._preheat_hotends: + self._preheat_hotends_timer.stop() def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration): position = extruder.getPosition() @@ -103,7 +135,20 @@ class USBPrinterOutputController(PrinterOutputController): return # Got invalid values, can't pre-heat. self.setTargetHotendTemperature(extruder.getPrinter(), position, temperature=temperature) - self._preheat_extruders_timer.setInterval(duration * 1000) - self._preheat_extruders_timer.start() - self._preheat_extruders.add(extruder) - extruder.updateIsPreheating(True) \ No newline at end of file + self._preheat_hotends_timer.setInterval(duration * 1000) + self._preheat_hotends_timer.start() + self._preheat_hotends.add(extruder) + extruder.updateIsPreheating(True) + + def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"): + self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), temperature=0) + if extruder in self._preheat_hotends: + extruder.updateIsPreheating(False) + self._preheat_hotends.remove(extruder) + if not self._preheat_hotends and self._preheat_hotends_timer.isActive(): + self._preheat_hotends_timer.stop() + + def _onPreheatHotendsTimerFinished(self): + for extruder in self._preheat_hotends: + self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0) + self._preheat_hotends = set() From f135b1675ccc427bf4825ced2da5a77f6b307874 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 18 Jan 2018 10:20:37 +0100 Subject: [PATCH 14/88] Fix disabling preheat button when printer is not idle --- resources/qml/PrinterOutput/ExtruderBox.qml | 9 ++++++--- resources/qml/PrinterOutput/HeatedBedBox.qml | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml index 4aacef1a94..56c86f1034 100644 --- a/resources/qml/PrinterOutput/ExtruderBox.qml +++ b/resources/qml/PrinterOutput/ExtruderBox.qml @@ -130,9 +130,12 @@ Item { return false; //Not allowed to do anything. } - if (connectedPrinter.jobState == "printing" || connectedPrinter.jobState == "pre_print" || connectedPrinter.jobState == "resuming" || connectedPrinter.jobState == "pausing" || connectedPrinter.jobState == "paused" || connectedPrinter.jobState == "error" || connectedPrinter.jobState == "offline") + if (connectedPrinter.activePrinter && connectedPrinter.activePrinter.activePrintJob) { - return false; //Printer is in a state where it can't react to pre-heating. + if((["printing", "pre_print", "resuming", "pausing", "paused", "error", "offline"]).indexOf(connectedPrinter.activePrinter.activePrintJob.state) != -1) + { + return false; //Printer is in a state where it can't react to pre-heating. + } } return true; } @@ -144,7 +147,7 @@ Item anchors.bottomMargin: UM.Theme.getSize("default_margin").height width: UM.Theme.getSize("monitor_preheat_temperature_control").width height: UM.Theme.getSize("monitor_preheat_temperature_control").height - visible: extruderModel != null ? extruderModel.canPreHeatHotends && !extruderModel.isPreheating : true + visible: extruderModel != null ? enabled && extruderModel.canPreHeatHotends && !extruderModel.isPreheating : true Rectangle //Highlight of input field. { anchors.fill: parent diff --git a/resources/qml/PrinterOutput/HeatedBedBox.qml b/resources/qml/PrinterOutput/HeatedBedBox.qml index 8d1e37b23d..385977141c 100644 --- a/resources/qml/PrinterOutput/HeatedBedBox.qml +++ b/resources/qml/PrinterOutput/HeatedBedBox.qml @@ -114,9 +114,12 @@ Item { return false; //Not allowed to do anything. } - if (connectedPrinter.jobState == "printing" || connectedPrinter.jobState == "pre_print" || connectedPrinter.jobState == "resuming" || connectedPrinter.jobState == "pausing" || connectedPrinter.jobState == "paused" || connectedPrinter.jobState == "error" || connectedPrinter.jobState == "offline") + if (connectedPrinter.activePrinter && connectedPrinter.activePrinter.activePrintJob) { - return false; //Printer is in a state where it can't react to pre-heating. + if((["printing", "pre_print", "resuming", "pausing", "paused", "error", "offline"]).indexOf(connectedPrinter.activePrinter.activePrintJob.state) != -1) + { + return false; //Printer is in a state where it can't react to pre-heating. + } } return true; } @@ -128,7 +131,7 @@ Item anchors.bottomMargin: UM.Theme.getSize("default_margin").height width: UM.Theme.getSize("monitor_preheat_temperature_control").width height: UM.Theme.getSize("monitor_preheat_temperature_control").height - visible: printerModel != null ? printerModel.canPreHeatBed && !printerModel.isPreheating : true + visible: printerModel != null ? enabled && printerModel.canPreHeatBed && !printerModel.isPreheating : true Rectangle //Highlight of input field. { anchors.fill: parent From fb2a5ea28a52c99830cb49503d26d609e51cbbd5 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 18 Jan 2018 11:55:30 +0100 Subject: [PATCH 15/88] Refactor USBPrinterOutputController to reusable cura.PrinterOutput.GenericOutputController --- .../PrinterOutput/GenericOutputController.py | 2 +- plugins/USBPrinting/USBPrinterOutputDevice.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) rename plugins/USBPrinting/USBPrinterOutputController.py => cura/PrinterOutput/GenericOutputController.py (99%) diff --git a/plugins/USBPrinting/USBPrinterOutputController.py b/cura/PrinterOutput/GenericOutputController.py similarity index 99% rename from plugins/USBPrinting/USBPrinterOutputController.py rename to cura/PrinterOutput/GenericOutputController.py index b59e60c244..106c1e3a44 100644 --- a/plugins/USBPrinting/USBPrinterOutputController.py +++ b/cura/PrinterOutput/GenericOutputController.py @@ -10,7 +10,7 @@ if MYPY: from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel -class USBPrinterOutputController(PrinterOutputController): +class GenericOutputController(PrinterOutputController): def __init__(self, output_device): super().__init__(output_device) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index b53f502d81..52cb0d47f5 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -10,9 +10,9 @@ from UM.PluginRegistry import PluginRegistry from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel +from cura.PrinterOutput.GenericOutputController import GenericOutputController from .AutoDetectBaudJob import AutoDetectBaudJob -from .USBPrinterOutputController import USBPrinterOutputController from .avr_isp import stk500v2, intelHex from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty @@ -237,7 +237,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): container_stack = Application.getInstance().getGlobalContainerStack() num_extruders = container_stack.getProperty("machine_extruder_count", "value") # Ensure that a printer is created. - self._printers = [PrinterOutputModel(output_controller=USBPrinterOutputController(self), number_of_extruders=num_extruders)] + self._printers = [PrinterOutputModel(output_controller=GenericOutputController(self), number_of_extruders=num_extruders)] self._printers[0].updateName(container_stack.getName()) self.setConnectionState(ConnectionState.connected) self._update_thread.start() @@ -364,7 +364,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): elapsed_time = int(time() - self._print_start_time) print_job = self._printers[0].activePrintJob if print_job is None: - print_job = PrintJobOutputModel(output_controller = USBPrinterOutputController(self), name= Application.getInstance().getPrintInformation().jobName) + print_job = PrintJobOutputModel(output_controller = GenericOutputController(self), name= Application.getInstance().getPrintInformation().jobName) print_job.updateState("printing") self._printers[0].updateActivePrintJob(print_job) From bcfcc8cc7c26f8cdc85ef3d69d9ea08cdd72838f Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 19 Jan 2018 14:59:25 +0100 Subject: [PATCH 16/88] Enable setting per mesh group settings when printing One at a Time --- .../PerObjectSettingsPanel.qml | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml index eb492d8de2..0976dd5df6 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml @@ -163,7 +163,16 @@ Item { id: addedSettingsModel; containerId: Cura.MachineManager.activeDefinitionId expanded: [ "*" ] - exclude: { + filter: + { + if (printSequencePropertyProvider.properties.value == "one_at_a_time") + { + return {"settable_per_meshgroup": true}; + } + return {"settable_per_mesh": true}; + } + exclude: + { var excluded_settings = [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]; if(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh") @@ -451,7 +460,11 @@ Item { containerId: Cura.MachineManager.activeDefinitionId filter: { - "settable_per_mesh": true + if (printSequencePropertyProvider.properties.value == "one_at_a_time") + { + return {"settable_per_meshgroup": true}; + } + return {"settable_per_mesh": true}; } visibilityHandler: UM.SettingPreferenceVisibilityHandler {} expanded: [ "*" ] @@ -507,6 +520,16 @@ Item { storeIndex: 0 } + UM.SettingPropertyProvider + { + id: printSequencePropertyProvider + + containerStackId: Cura.MachineManager.activeMachineId + key: "print_sequence" + watchedProperties: [ "value" ] + storeIndex: 0 + } + SystemPalette { id: palette; } Component From 2703474861f50269228fd34f0ef2d4dc5cbb645c Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Sat, 27 Jan 2018 21:49:19 +0100 Subject: [PATCH 17/88] Only select all text when tabbing through fields, not when clicking a field --- resources/qml/Settings/SettingTextField.qml | 33 +++++++++++++++------ 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/resources/qml/Settings/SettingTextField.qml b/resources/qml/Settings/SettingTextField.qml index 9056bbe45a..2226e1f09d 100644 --- a/resources/qml/Settings/SettingTextField.qml +++ b/resources/qml/Settings/SettingTextField.qml @@ -13,11 +13,17 @@ SettingItem property string textBeforeEdit property bool textHasChanged + property bool focusGainedByClick: false onFocusReceived: { textHasChanged = false; textBeforeEdit = focusItem.text; - focusItem.selectAll(); + + if(!focusGainedByClick) + { + // select all text when tabbing through fields (but not when selecting a field with the mouse) + focusItem.selectAll(); + } } contents: Rectangle @@ -92,14 +98,6 @@ SettingItem font: UM.Theme.getFont("default") } - MouseArea - { - id: mouseArea - anchors.fill: parent; - //hoverEnabled: true; - cursorShape: Qt.IBeamCursor - } - TextInput { id: input @@ -141,6 +139,7 @@ SettingItem { base.focusReceived(); } + base.focusGainedByClick = false; } color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text") @@ -177,6 +176,22 @@ SettingItem } when: !input.activeFocus } + + MouseArea + { + id: mouseArea + anchors.fill: parent; + + cursorShape: Qt.IBeamCursor + + onPressed: { + if(!input.activeFocus) { + base.focusGainedByClick = true; + input.forceActiveFocus(); + } + mouse.accepted = false; + } + } } } } From 4d5ace4564cb1a927312665f3c63321164da8be8 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sun, 11 Mar 2018 10:27:57 -0400 Subject: [PATCH 18/88] Added default material diameter to SeeMeCNC printer definitions --- resources/definitions/seemecnc_artemis.def.json | 1 + resources/definitions/seemecnc_v32.def.json | 1 + 2 files changed, 2 insertions(+) diff --git a/resources/definitions/seemecnc_artemis.def.json b/resources/definitions/seemecnc_artemis.def.json index 88a1b28de6..0b31abfa41 100644 --- a/resources/definitions/seemecnc_artemis.def.json +++ b/resources/definitions/seemecnc_artemis.def.json @@ -25,6 +25,7 @@ "machine_nozzle_size": { "default_value": 0.5 }, "machine_shape": { "default_value": "elliptic" }, "machine_width": { "default_value": 290 }, + "material_diameter": { "default_value": 1.75 }, "relative_extrusion": { "default_value": false }, "retraction_amount": { "default_value": 3.2 }, "retraction_combing": { "default_value": "off" }, diff --git a/resources/definitions/seemecnc_v32.def.json b/resources/definitions/seemecnc_v32.def.json index 5932403bc5..3f46c1540a 100644 --- a/resources/definitions/seemecnc_v32.def.json +++ b/resources/definitions/seemecnc_v32.def.json @@ -25,6 +25,7 @@ "machine_nozzle_size": { "default_value": 0.5 }, "machine_shape": { "default_value": "elliptic" }, "machine_width": { "default_value": 265 }, + "material_diameter": { "default_value": 1.75 }, "relative_extrusion": { "default_value": false }, "retraction_amount": { "default_value": 3.2 }, "retraction_combing": { "default_value": "off" }, From 0caea24afcb8d94fbf32ef330a0d2cc6788be16f Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 13 Mar 2018 11:54:32 +0100 Subject: [PATCH 19/88] Add depth pass for picking a location --- cura/DepthPass.py | 60 +++++++++++++++++ plugins/SupportEraser/SupportEraser.py | 27 +++++--- resources/shaders/camera_distance.shader | 83 ++++++++++++++++++++++++ 3 files changed, 160 insertions(+), 10 deletions(-) create mode 100644 cura/DepthPass.py create mode 100644 resources/shaders/camera_distance.shader diff --git a/cura/DepthPass.py b/cura/DepthPass.py new file mode 100644 index 0000000000..3fb57b0341 --- /dev/null +++ b/cura/DepthPass.py @@ -0,0 +1,60 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from UM.Application import Application +from UM.Math.Color import Color +from UM.Resources import Resources + +from UM.View.RenderPass import RenderPass +from UM.View.GL.OpenGL import OpenGL +from UM.View.RenderBatch import RenderBatch + +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator + + +## A RenderPass subclass that renders a depthmap of selectable objects to a texture. +# It uses the active camera by default, but it can be overridden to use a different camera. +# +# Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels +class DepthPass(RenderPass): + def __init__(self, width: int, height: int): + super().__init__("preview", width, height, 0) + + self._renderer = Application.getInstance().getRenderer() + + self._shader = None + self._scene = Application.getInstance().getController().getScene() + + + def render(self) -> None: + if not self._shader: + self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "camera_distance.shader")) + + self._gl.glClearColor(0.0, 0.0, 0.0, 0.0) + self._gl.glClear(self._gl.GL_COLOR_BUFFER_BIT | self._gl.GL_DEPTH_BUFFER_BIT) + + # Create a new batch to be rendered + batch = RenderBatch(self._shader) + + # Fill up the batch with objects that can be sliced. ` + for node in DepthFirstIterator(self._scene.getRoot()): + if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): + batch.addItem(node.getWorldTransformation(), node.getMeshData()) + + self.bind() + batch.render(self._scene.getActiveCamera()) + self.release() + + ## Get the distance in mm from the camera to at a certain pixel coordinate. + def getDepthAtPosition(self, x, y): + output = self.getOutput() + + window_size = self._renderer.getWindowSize() + + px = (0.5 + x / 2.0) * window_size[0] + py = (0.5 + y / 2.0) * window_size[1] + + if px < 0 or px > (output.width() - 1) or py < 0 or py > (output.height() - 1): + return None + + distance = output.pixel(px, py) # distance in micron, from in r, g & b channels + return distance / 1000. diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 8b3ad0f4dd..119e598886 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -4,14 +4,16 @@ from UM.Math.Vector import Vector from UM.Tool import Tool from PyQt5.QtCore import Qt, QUrl from UM.Application import Application -from UM.Event import Event +from UM.Event import Event, MouseEvent from UM.Mesh.MeshBuilder import MeshBuilder from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Settings.SettingInstance import SettingInstance from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator +from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator +from cura.DepthPass import DepthPass import os import os.path @@ -25,15 +27,22 @@ class SupportEraser(Tool): def event(self, event): super().event(event) - if event.type == Event.ToolActivateEvent: + if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): + active_camera = self._controller.getScene().getActiveCamera() - # Load the remover mesh: - self._createEraserMesh() + # Create depth pass for picking + render_width, render_height = active_camera.getWindowSize() + depth_pass = DepthPass(int(render_width), int(render_height)) + depth_pass.render() - # After we load the mesh, deactivate the tool again: - self.getController().setActiveTool(None) + distance = depth_pass.getDepthAtPosition(event.x, event.y) + ray = active_camera.getRay(event.x, event.y) + picked_position = ray.getPointAlongRay(distance) - def _createEraserMesh(self): + # Add the anto_overhang_mesh cube: + self._createEraserMesh(picked_position) + + def _createEraserMesh(self, position: Vector): node = CuraSceneNode() node.setName("Eraser") @@ -41,9 +50,7 @@ class SupportEraser(Tool): mesh = MeshBuilder() mesh.addCube(10,10,10) node.setMeshData(mesh.build()) - # Place the cube in the platform. Do it manually so it works if the "automatic drop models" is OFF - move_vector = Vector(0, 5, 0) - node.setPosition(move_vector) + node.setPosition(position) active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate diff --git a/resources/shaders/camera_distance.shader b/resources/shaders/camera_distance.shader new file mode 100644 index 0000000000..2dd90e7f15 --- /dev/null +++ b/resources/shaders/camera_distance.shader @@ -0,0 +1,83 @@ +[shaders] +vertex = + uniform highp mat4 u_modelMatrix; + uniform highp mat4 u_viewProjectionMatrix; + + attribute highp vec4 a_vertex; + + varying highp vec3 v_vertex; + + void main() + { + vec4 world_space_vert = u_modelMatrix * a_vertex; + gl_Position = u_viewProjectionMatrix * world_space_vert; + + v_vertex = world_space_vert.xyz; + } + +fragment = + uniform highp vec3 u_viewPosition; + + varying highp vec3 v_vertex; + + void main() + { + highp float distance_to_camera = distance(v_vertex, u_viewPosition) * 1000.; // distance in micron + + vec3 encoded; // encode float into 3 8-bit channels; this gives a precision of a micron at a range of up to ~16 meter + encoded.b = floor(distance_to_camera / 65536.0); + encoded.g = floor((distance_to_camera - encoded.b * 65536.0) / 256.0); + encoded.r = floor(distance_to_camera - encoded.b * 65536.0 - encoded.g * 256.0); + + gl_FragColor.rgb = encoded / 255.; + gl_FragColor.a = 1.0; + } + +vertex41core = + #version 410 + uniform highp mat4 u_modelMatrix; + uniform highp mat4 u_viewProjectionMatrix; + + in highp vec4 a_vertex; + + out highp vec3 v_vertex; + + void main() + { + vec4 world_space_vert = u_modelMatrix * a_vertex; + gl_Position = u_viewProjectionMatrix * world_space_vert; + + v_vertex = world_space_vert.xyz; + } + +fragment41core = + #version 410 + uniform highp vec3 u_viewPosition; + + in highp vec3 v_vertex; + + out vec4 frag_color; + + void main() + { + highp float distance_to_camera = distance(v_vertex, u_viewPosition) * 1000.; // distance in micron + + vec3 encoded; // encode float into 3 8-bit channels; this gives a precision of a micron at a range of up to ~16 meter + encoded.b = floor(distance_to_camera / 65536.0); + encoded.g = floor((distance_to_camera - encoded.b * 65536.0) / 256.0); + encoded.r = floor(distance_to_camera - encoded.b * 65536.0 - encoded.g * 256.0); + + frag_color.rgb = encoded / 255.; + frag_color.a = 1.0; + } + +[defaults] + +[bindings] +u_modelMatrix = model_matrix +u_viewProjectionMatrix = view_projection_matrix +u_normalMatrix = normal_matrix +u_viewPosition = view_position + +[attributes] +a_vertex = vertex From 73558c9e3695335008cda045c4293e837e1ef3f0 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 13 Mar 2018 19:44:53 +0100 Subject: [PATCH 20/88] Fix rendering depth pass --- cura/DepthPass.py | 9 ++++++--- plugins/SupportEraser/SupportEraser.py | 3 +-- resources/shaders/camera_distance.shader | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/cura/DepthPass.py b/cura/DepthPass.py index 3fb57b0341..a2cfdbaab0 100644 --- a/cura/DepthPass.py +++ b/cura/DepthPass.py @@ -17,7 +17,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator # Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels class DepthPass(RenderPass): def __init__(self, width: int, height: int): - super().__init__("preview", width, height, 0) + super().__init__("depth", width, height) self._renderer = Application.getInstance().getRenderer() @@ -29,7 +29,9 @@ class DepthPass(RenderPass): if not self._shader: self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "camera_distance.shader")) - self._gl.glClearColor(0.0, 0.0, 0.0, 0.0) + width, height = self.getSize() + self._gl.glViewport(0, 0, width, height) + self._gl.glClearColor(1.0, 1.0, 1.0, 0.0) self._gl.glClear(self._gl.GL_COLOR_BUFFER_BIT | self._gl.GL_DEPTH_BUFFER_BIT) # Create a new batch to be rendered @@ -57,4 +59,5 @@ class DepthPass(RenderPass): return None distance = output.pixel(px, py) # distance in micron, from in r, g & b channels - return distance / 1000. + distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm + return distance diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 119e598886..4034b524b8 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -31,8 +31,7 @@ class SupportEraser(Tool): active_camera = self._controller.getScene().getActiveCamera() # Create depth pass for picking - render_width, render_height = active_camera.getWindowSize() - depth_pass = DepthPass(int(render_width), int(render_height)) + depth_pass = DepthPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) depth_pass.render() distance = depth_pass.getDepthAtPosition(event.x, event.y) diff --git a/resources/shaders/camera_distance.shader b/resources/shaders/camera_distance.shader index 2dd90e7f15..e6e894a2f6 100644 --- a/resources/shaders/camera_distance.shader +++ b/resources/shaders/camera_distance.shader @@ -63,9 +63,9 @@ fragment41core = highp float distance_to_camera = distance(v_vertex, u_viewPosition) * 1000.; // distance in micron vec3 encoded; // encode float into 3 8-bit channels; this gives a precision of a micron at a range of up to ~16 meter - encoded.b = floor(distance_to_camera / 65536.0); - encoded.g = floor((distance_to_camera - encoded.b * 65536.0) / 256.0); - encoded.r = floor(distance_to_camera - encoded.b * 65536.0 - encoded.g * 256.0); + encoded.r = floor(distance_to_camera / 65536.0); + encoded.g = floor((distance_to_camera - encoded.r * 65536.0) / 256.0); + encoded.b = floor(distance_to_camera - encoded.r * 65536.0 - encoded.g * 256.0); frag_color.rgb = encoded / 255.; frag_color.a = 1.0; From d88724aff539748dbdb0001bacbe6785cae68ba1 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 13 Mar 2018 20:05:49 +0100 Subject: [PATCH 21/88] Move ray picking to DepthPass --- cura/DepthPass.py | 15 +++++++++++---- plugins/SupportEraser/SupportEraser.py | 4 +--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/cura/DepthPass.py b/cura/DepthPass.py index a2cfdbaab0..4435d533ff 100644 --- a/cura/DepthPass.py +++ b/cura/DepthPass.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from UM.Application import Application -from UM.Math.Color import Color +from UM.Math.Vector import Vector from UM.Resources import Resources from UM.View.RenderPass import RenderPass @@ -11,8 +11,8 @@ from UM.View.RenderBatch import RenderBatch from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator -## A RenderPass subclass that renders a depthmap of selectable objects to a texture. -# It uses the active camera by default, but it can be overridden to use a different camera. +## A RenderPass subclass that renders a the distance of selectable objects from the active camera to a texture. +# The texture is used to map a 2d location (eg the mouse location) to a world space position # # Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels class DepthPass(RenderPass): @@ -47,7 +47,7 @@ class DepthPass(RenderPass): self.release() ## Get the distance in mm from the camera to at a certain pixel coordinate. - def getDepthAtPosition(self, x, y): + def getPickedDepth(self, x, y) -> float: output = self.getOutput() window_size = self._renderer.getWindowSize() @@ -61,3 +61,10 @@ class DepthPass(RenderPass): distance = output.pixel(px, py) # distance in micron, from in r, g & b channels distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm return distance + + ## Get the world coordinates of a picked point + def getPickedPosition(self, x, y) -> Vector: + distance = self.getPickedDepth(x, y) + ray = self._scene.getActiveCamera().getRay(x, y) + + return ray.getPointAlongRay(distance) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 4034b524b8..0ddfed0cf1 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -34,9 +34,7 @@ class SupportEraser(Tool): depth_pass = DepthPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) depth_pass.render() - distance = depth_pass.getDepthAtPosition(event.x, event.y) - ray = active_camera.getRay(event.x, event.y) - picked_position = ray.getPointAlongRay(distance) + picked_position = depth_pass.getPickedPosition(event.x, event.y) # Add the anto_overhang_mesh cube: self._createEraserMesh(picked_position) From a536da503bf374f5db0fec5180e9989294f77c22 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Tue, 13 Mar 2018 20:40:41 +0100 Subject: [PATCH 22/88] Rename DepthPass to PickingPass The map created by the shader is not strictly a depth map; not only is the "depth" encoded in the rgb channels, but it is also a distance to the camera instead of a "scene depth". --- cura/{DepthPass.py => PickingPass.py} | 4 ++-- plugins/SupportEraser/SupportEraser.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) rename cura/{DepthPass.py => PickingPass.py} (97%) diff --git a/cura/DepthPass.py b/cura/PickingPass.py similarity index 97% rename from cura/DepthPass.py rename to cura/PickingPass.py index 4435d533ff..4bd893e926 100644 --- a/cura/DepthPass.py +++ b/cura/PickingPass.py @@ -15,9 +15,9 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator # The texture is used to map a 2d location (eg the mouse location) to a world space position # # Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels -class DepthPass(RenderPass): +class PickingPass(RenderPass): def __init__(self, width: int, height: int): - super().__init__("depth", width, height) + super().__init__("picking", width, height) self._renderer = Application.getInstance().getRenderer() diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 0ddfed0cf1..35713805bc 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -13,7 +13,7 @@ from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -from cura.DepthPass import DepthPass +from cura.PickingPass import PickingPass import os import os.path @@ -30,13 +30,13 @@ class SupportEraser(Tool): if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): active_camera = self._controller.getScene().getActiveCamera() - # Create depth pass for picking - depth_pass = DepthPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) - depth_pass.render() + # Create a pass for picking a world-space location from the mouse location + picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) + picking_pass.render() - picked_position = depth_pass.getPickedPosition(event.x, event.y) + picked_position = picking_pass.getPickedPosition(event.x, event.y) - # Add the anto_overhang_mesh cube: + # Add the anti_overhang_mesh cube at the picked location self._createEraserMesh(picked_position) def _createEraserMesh(self, position: Vector): From 7e4cb1c36ecb4aed44bc9da4aab676eed21e1ae3 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Sun, 11 Mar 2018 13:06:30 +0100 Subject: [PATCH 23/88] Disable Support Eraser if anti_overhang_mesh is disabled --- plugins/SupportEraser/SupportEraser.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 35713805bc..65d22bcdfd 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -24,6 +24,8 @@ class SupportEraser(Tool): self._shortcut_key = Qt.Key_G self._controller = Application.getInstance().getController() + Application.getInstance().globalContainerStackChanged.connect(self._updateEnabled) + def event(self, event): super().event(event) @@ -73,3 +75,12 @@ class SupportEraser(Tool): op = AddSceneNodeOperation(node, scene.getRoot()) op.push() Application.getInstance().getController().getScene().sceneChanged.emit(node) + + def _updateEnabled(self): + plugin_enabled = False + + global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack: + plugin_enabled = global_container_stack.getProperty("anti_overhang_mesh", "enabled") + + Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled) From 61ce0c3154a2398f775b0a0c92865adf6b19f05f Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 14 Mar 2018 15:15:43 +0100 Subject: [PATCH 24/88] CURA-4870 Make the configuration pop-up open and close with one click. Before it was the case in which sometimes the user needs two clicks for that. Also collapse the dropdown when the configuration is applied. --- .../ConfigurationListView.qml | 1 + .../ConfigurationSelection.qml | 62 +++++++++---------- .../Menus/ConfigurationMenu/SyncButton.qml | 5 -- 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml index 52fd0e6556..999fecd7fd 100644 --- a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml @@ -66,6 +66,7 @@ Column configuration: modelData onActivateConfiguration: { + switchPopupState() Cura.MachineManager.applyRemoteConfiguration(configuration) } } diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml index eb0d5f5cff..a3cf10168b 100644 --- a/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml @@ -13,54 +13,54 @@ Item id: configurationSelector property var connectedDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null property var panelWidth: control.width - property var panelVisible: false - SyncButton { - onClicked: configurationSelector.state == "open" ? configurationSelector.state = "closed" : configurationSelector.state = "open" + function switchPopupState() + { + popup.opened ? popup.close() : popup.open() + } + + SyncButton + { + id: syncButton + onClicked: switchPopupState() outputDevice: connectedDevice } - Popup { + Popup + { + // TODO Change once updating to Qt5.10 - This property is already in 5.10 but is manually implemented until upgrade + property bool opened: false id: popup clip: true + closePolicy: Popup.CloseOnPressOutsideParent y: configurationSelector.height - UM.Theme.getSize("default_lining").height x: configurationSelector.width - width width: panelWidth - visible: panelVisible && connectedDevice != null + visible: opened padding: UM.Theme.getSize("default_lining").width - contentItem: ConfigurationListView { + transformOrigin: Popup.Top + contentItem: ConfigurationListView + { id: configList width: panelWidth - 2 * popup.padding outputDevice: connectedDevice } - background: Rectangle { + background: Rectangle + { color: UM.Theme.getColor("setting_control") border.color: UM.Theme.getColor("setting_control_border") } - } - - states: [ - // This adds a second state to the container where the rectangle is farther to the right - State { - name: "open" - PropertyChanges { - target: popup - height: configList.computedHeight - } - }, - State { - name: "closed" - PropertyChanges { - target: popup - height: 0 - } - } - ] - transitions: [ - // This adds a transition that defaults to applying to all state changes - Transition { + exit: Transition + { // This applies a default NumberAnimation to any changes a state change makes to x or y properties - NumberAnimation { properties: "height"; duration: 200; easing.type: Easing.InOutQuad; } + NumberAnimation { property: "visible"; duration: 75; } } - ] + enter: Transition + { + // This applies a default NumberAnimation to any changes a state change makes to x or y properties + NumberAnimation { property: "visible"; duration: 75; } + } + onClosed: opened = false + onOpened: opened = true + } } \ No newline at end of file diff --git a/resources/qml/Menus/ConfigurationMenu/SyncButton.qml b/resources/qml/Menus/ConfigurationMenu/SyncButton.qml index f0394cc107..c292a792db 100644 --- a/resources/qml/Menus/ConfigurationMenu/SyncButton.qml +++ b/resources/qml/Menus/ConfigurationMenu/SyncButton.qml @@ -86,11 +86,6 @@ Button label: Label {} } - onClicked: - { - panelVisible = !panelVisible - } - Connections { target: outputDevice onUniqueConfigurationsChanged: { From e3dd7a449d396cadd97835cb531fe74dc0264b13 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 14 Mar 2018 16:09:59 +0100 Subject: [PATCH 25/88] CURA-5090 speedups by using qtimers on updating mostly visual elements --- cura/BuildVolume.py | 2 +- cura/CuraApplication.py | 15 +++++++++++--- cura/Machines/Models/MultiBuildPlateModel.py | 12 +++++++++-- cura/ObjectsModel.py | 17 ++++++++++++++-- cura/Scene/ConvexHullDecorator.py | 21 +++++++++++++++++++- plugins/SimulationView/SimulationView.py | 7 ++++--- 6 files changed, 62 insertions(+), 12 deletions(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 0b81a5183f..d93ce1107d 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -136,6 +136,7 @@ class BuildVolume(SceneNode): if active_extruder_changed is not None: node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild) node.decoratorsChanged.disconnect(self._updateNodeListeners) + self._updateDisallowedAreasAndRebuild() # make sure we didn't miss anything before we updated the node listeners self._scene_objects = new_scene_objects self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered. @@ -150,7 +151,6 @@ class BuildVolume(SceneNode): active_extruder_changed = node.callDecoration("getActiveExtruderChangedSignal") if active_extruder_changed is not None: active_extruder_changed.connect(self._updateDisallowedAreasAndRebuild) - self._updateDisallowedAreasAndRebuild() def setWidth(self, width): if width is not None: diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 84ec787cd7..99cc30a12b 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -4,7 +4,7 @@ #Type hinting. from typing import Dict -from PyQt5.QtCore import QObject +from PyQt5.QtCore import QObject, QTimer from PyQt5.QtNetwork import QLocalServer from PyQt5.QtNetwork import QLocalSocket @@ -283,10 +283,15 @@ class CuraApplication(QtApplication): self._preferred_mimetype = "" self._i18n_catalog = i18nCatalog("cura") - self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity) + self._update_platform_activity_timer = QTimer() + self._update_platform_activity_timer.setInterval(500) + self._update_platform_activity_timer.setSingleShot(True) + self._update_platform_activity_timer.timeout.connect(self.updatePlatformActivity) + + self.getController().getScene().sceneChanged.connect(self.updatePlatformActivityDelayed) self.getController().toolOperationStopped.connect(self._onToolOperationStopped) self.getController().contextMenuRequested.connect(self._onContextMenuRequested) - self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivity) + self.getCuraSceneController().activeBuildPlateChanged.connect(self.updatePlatformActivityDelayed) Resources.addType(self.ResourceTypes.QmlFiles, "qml") Resources.addType(self.ResourceTypes.Firmware, "firmware") @@ -1061,6 +1066,10 @@ class CuraApplication(QtApplication): def getSceneBoundingBoxString(self): return self._i18n_catalog.i18nc("@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm.", "%(width).1f x %(depth).1f x %(height).1f mm") % {'width' : self._scene_bounding_box.width.item(), 'depth': self._scene_bounding_box.depth.item(), 'height' : self._scene_bounding_box.height.item()} + def updatePlatformActivityDelayed(self, node = None): + if node is not None and node.getMeshData() is not None: + self._update_platform_activity_timer.start() + ## Update scene bounding box for current build plate def updatePlatformActivity(self, node = None): count = 0 diff --git a/cura/Machines/Models/MultiBuildPlateModel.py b/cura/Machines/Models/MultiBuildPlateModel.py index f0f4997014..958e93837a 100644 --- a/cura/Machines/Models/MultiBuildPlateModel.py +++ b/cura/Machines/Models/MultiBuildPlateModel.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from PyQt5.QtCore import pyqtSignal, pyqtProperty +from PyQt5.QtCore import QTimer, pyqtSignal, pyqtProperty from UM.Application import Application from UM.Scene.Selection import Selection @@ -21,8 +21,13 @@ class MultiBuildPlateModel(ListModel): def __init__(self, parent = None): super().__init__(parent) + self._update_timer = QTimer() + self._update_timer.setInterval(100) + self._update_timer.setSingleShot(True) + self._update_timer.timeout.connect(self._updateSelectedObjectBuildPlateNumbers) + self._application = Application.getInstance() - self._application.getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbers) + self._application.getController().getScene().sceneChanged.connect(self._updateSelectedObjectBuildPlateNumbersDelayed) Selection.selectionChanged.connect(self._updateSelectedObjectBuildPlateNumbers) self._max_build_plate = 1 # default @@ -45,6 +50,9 @@ class MultiBuildPlateModel(ListModel): def activeBuildPlate(self): return self._active_build_plate + def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args): + self._update_timer.start() + def _updateSelectedObjectBuildPlateNumbers(self, *args): result = set() for node in Selection.getAllSelectedObjects(): diff --git a/cura/ObjectsModel.py b/cura/ObjectsModel.py index f02e8b4db5..cfe4320e28 100644 --- a/cura/ObjectsModel.py +++ b/cura/ObjectsModel.py @@ -1,3 +1,8 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from PyQt5.QtCore import QTimer + from UM.Application import Application from UM.Qt.ListModel import ListModel from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator @@ -14,8 +19,13 @@ class ObjectsModel(ListModel): def __init__(self): super().__init__() - Application.getInstance().getController().getScene().sceneChanged.connect(self._update) - Preferences.getInstance().preferenceChanged.connect(self._update) + Application.getInstance().getController().getScene().sceneChanged.connect(self._updateDelayed) + Preferences.getInstance().preferenceChanged.connect(self._updateDelayed) + + self._update_timer = QTimer() + self._update_timer.setInterval(100) + self._update_timer.setSingleShot(True) + self._update_timer.timeout.connect(self._update) self._build_plate_number = -1 @@ -23,6 +33,9 @@ class ObjectsModel(ListModel): self._build_plate_number = nr self._update() + def _updateDelayed(self, *args): + self._update_timer.start() + def _update(self, *args): nodes = [] filter_current_build_plate = Preferences.getInstance().getValue("view/filter_current_build_plate") diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index 3a563c2764..66bc8a7fc3 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -1,6 +1,8 @@ # Copyright (c) 2016 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from PyQt5.QtCore import QTimer + from UM.Application import Application from UM.Math.Polygon import Polygon from UM.Scene.SceneNodeDecorator import SceneNodeDecorator @@ -22,6 +24,10 @@ class ConvexHullDecorator(SceneNodeDecorator): self._global_stack = None + # Make sure the timer is created on the main thread + self._recompute_convex_hull_timer = None + Application.getInstance().callLater(self.createRecomputeConvexHullTimer) + self._raft_thickness = 0.0 # For raft thickness, DRY self._build_volume = Application.getInstance().getBuildVolume() @@ -33,6 +39,12 @@ class ConvexHullDecorator(SceneNodeDecorator): self._onGlobalStackChanged() + def createRecomputeConvexHullTimer(self): + self._recompute_convex_hull_timer = QTimer() + self._recompute_convex_hull_timer.setInterval(200) + self._recompute_convex_hull_timer.setSingleShot(True) + self._recompute_convex_hull_timer.timeout.connect(self.recomputeConvexHull) + def setNode(self, node): previous_node = self._node # Disconnect from previous node signals @@ -99,6 +111,12 @@ class ConvexHullDecorator(SceneNodeDecorator): return self._compute2DConvexHull() return None + def recomputeConvexHullDelayed(self): + if self._recompute_convex_hull_timer is not None: + self._recompute_convex_hull_timer.start() + else: + self.recomputeConvexHull() + def recomputeConvexHull(self): controller = Application.getInstance().getController() root = controller.getScene().getRoot() @@ -279,7 +297,8 @@ class ConvexHullDecorator(SceneNodeDecorator): def _onChanged(self, *args): self._raft_thickness = self._build_volume.getRaftThickness() - self.recomputeConvexHull() + if not args or args[0] == self._node: + self.recomputeConvexHullDelayed() def _onGlobalStackChanged(self): if self._global_stack: diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 35ce9cc37a..5c3dca9fae 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -158,9 +158,10 @@ class SimulationView(View): return self._nozzle_node def _onSceneChanged(self, node): - self.setActivity(False) - self.calculateMaxLayers() - self.calculateMaxPathsOnLayer(self._current_layer_num) + if node.getMeshData() is not None: + self.setActivity(False) + self.calculateMaxLayers() + self.calculateMaxPathsOnLayer(self._current_layer_num) def isBusy(self): return self._busy From 6909c9263a050010f1f69a5b66930ad50641ce4c Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Wed, 14 Mar 2018 16:32:53 +0100 Subject: [PATCH 26/88] CURA-4400 fix setting extruder number on a group printed with mixed extruders --- cura/CuraActions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cura/CuraActions.py b/cura/CuraActions.py index f517ec4217..75338f17b6 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -109,10 +109,6 @@ class CuraActions(QObject): nodes_to_change = [] for node in Selection.getAllSelectedObjects(): - # Do not change any nodes that already have the right extruder set. - if node.callDecoration("getActiveExtruder") == extruder_id: - continue - # If the node is a group, apply the active extruder to all children of the group. if node.callDecoration("isGroup"): for grouped_node in BreadthFirstIterator(node): @@ -125,6 +121,10 @@ class CuraActions(QObject): nodes_to_change.append(grouped_node) continue + # Do not change any nodes that already have the right extruder set. + if node.callDecoration("getActiveExtruder") == extruder_id: + continue + nodes_to_change.append(node) if not nodes_to_change: From 2fdd51fc231cadb2c1ee8d6eacfccf2b1fd7c105 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 14 Mar 2018 16:47:01 +0100 Subject: [PATCH 27/88] CURA-4870 Bind the network information with the output devices changed signal. --- cura/Settings/MachineManager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 3af6f70e5f..c79d352dcb 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -467,13 +467,13 @@ class MachineManager(QObject): return self._global_container_stack.getId() return "" - @pyqtProperty(str, notify = globalContainerChanged) + @pyqtProperty(str, notify = outputDevicesChanged) def activeMachineNetworkKey(self) -> str: if self._global_container_stack: return self._global_container_stack.getMetaDataEntry("um_network_key") return "" - @pyqtProperty(str, notify = globalContainerChanged) + @pyqtProperty(str, notify = outputDevicesChanged) def activeMachineNetworkGroupName(self) -> str: if self._global_container_stack: return self._global_container_stack.getMetaDataEntry("connect_group_name") From e6c5bd28b2de4b4759cbd3e643882b927675a2e5 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 14 Mar 2018 16:53:51 +0100 Subject: [PATCH 28/88] CURA-4870 Use the visible property instead of a Qt5.10 existing 'opened' property. This is needed to avoid a crashing for those that are using 5.10 running from source. --- .../ConfigurationMenu/ConfigurationSelection.qml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml index a3cf10168b..d7ee2c68ee 100644 --- a/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationSelection.qml @@ -16,7 +16,7 @@ Item function switchPopupState() { - popup.opened ? popup.close() : popup.open() + popup.visible ? popup.close() : popup.open() } SyncButton @@ -28,15 +28,14 @@ Item Popup { - // TODO Change once updating to Qt5.10 - This property is already in 5.10 but is manually implemented until upgrade - property bool opened: false + // TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property id: popup clip: true closePolicy: Popup.CloseOnPressOutsideParent y: configurationSelector.height - UM.Theme.getSize("default_lining").height x: configurationSelector.width - width width: panelWidth - visible: opened + visible: false padding: UM.Theme.getSize("default_lining").width transformOrigin: Popup.Top contentItem: ConfigurationListView @@ -60,7 +59,7 @@ Item // This applies a default NumberAnimation to any changes a state change makes to x or y properties NumberAnimation { property: "visible"; duration: 75; } } - onClosed: opened = false - onOpened: opened = true + onClosed: visible = false + onOpened: visible = true } } \ No newline at end of file From 1cf832653ce5012de855bc2e3d5833c968ecf7a6 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Wed, 14 Mar 2018 17:00:39 +0100 Subject: [PATCH 29/88] CURA-4870 Make the configuration selector only available for network printers. Show the connection icon also for printers connected via USB or Octoprint. --- resources/qml/MachineSelection.qml | 37 +++++++++++++++--------------- resources/qml/Sidebar.qml | 8 ++++--- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/resources/qml/MachineSelection.qml b/resources/qml/MachineSelection.qml index b3f9629703..357e7870a7 100644 --- a/resources/qml/MachineSelection.qml +++ b/resources/qml/MachineSelection.qml @@ -10,17 +10,22 @@ import UM 1.2 as UM import Cura 1.0 as Cura import "Menus" -ToolButton { +ToolButton +{ id: base property bool isNetworkPrinter: Cura.MachineManager.activeMachineNetworkKey != "" + property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0 property var printerStatus: Cura.MachineManager.printerOutputDevices.length != 0 ? "connected" : "disconnected" text: isNetworkPrinter ? Cura.MachineManager.activeMachineNetworkGroupName : Cura.MachineManager.activeMachineName tooltip: Cura.MachineManager.activeMachineName - style: ButtonStyle { - background: Rectangle { - color: { + style: ButtonStyle + { + background: Rectangle + { + color: + { if (control.pressed) { return UM.Theme.getColor("sidebar_header_active"); } @@ -33,7 +38,8 @@ ToolButton { } Behavior on color { ColorAnimation { duration: 50; } } - UM.RecolorImage { + UM.RecolorImage + { id: downArrow anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right @@ -46,18 +52,21 @@ ToolButton { source: UM.Theme.getIcon("arrow_bottom") } - PrinterStatusIcon { + PrinterStatusIcon + { id: printerStatusIcon - visible: isNetworkPrinter + visible: printerConnected status: printerStatus - anchors { + anchors + { verticalCenter: parent.verticalCenter left: parent.left leftMargin: UM.Theme.getSize("sidebar_margin").width } } - Label { + Label + { id: sidebarComboBoxLabel color: UM.Theme.getColor("sidebar_header_text_active") text: control.text; @@ -74,14 +83,4 @@ ToolButton { } menu: PrinterMenu { } - - // Make the toolbutton react when the outputdevice changes - Connections - { - target: Cura.MachineManager - onOutputDevicesChanged: - { - base.isNetworkPrinter = Cura.MachineManager.activeMachineNetworkKey != "" - } - } } diff --git a/resources/qml/Sidebar.qml b/resources/qml/Sidebar.qml index 5211ee5a1d..4744bbfda0 100644 --- a/resources/qml/Sidebar.qml +++ b/resources/qml/Sidebar.qml @@ -87,7 +87,8 @@ Rectangle } } - MachineSelection { + MachineSelection + { id: machineSelection width: base.width - configSelection.width - separator.width height: UM.Theme.getSize("sidebar_header").height @@ -105,9 +106,10 @@ Rectangle anchors.left: machineSelection.right } - ConfigurationSelection { + ConfigurationSelection + { id: configSelection - visible: isNetworkPrinter && !sidebar.monitoringPrint && !sidebar.hideSettings + visible: isNetworkPrinter width: visible ? Math.round(base.width * 0.15) : 0 height: UM.Theme.getSize("sidebar_header").height anchors.top: base.top From e4a416258b96c2927adba04d0c8ffd89abc3a8a7 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 09:03:50 +0100 Subject: [PATCH 30/88] Fix code-style and type hinting --- cura/PickingPass.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cura/PickingPass.py b/cura/PickingPass.py index 4bd893e926..2a1abe8f63 100644 --- a/cura/PickingPass.py +++ b/cura/PickingPass.py @@ -24,7 +24,6 @@ class PickingPass(RenderPass): self._shader = None self._scene = Application.getInstance().getController().getScene() - def render(self) -> None: if not self._shader: self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "camera_distance.shader")) @@ -47,7 +46,7 @@ class PickingPass(RenderPass): self.release() ## Get the distance in mm from the camera to at a certain pixel coordinate. - def getPickedDepth(self, x, y) -> float: + def getPickedDepth(self, x: int, y: int) -> float: output = self.getOutput() window_size = self._renderer.getWindowSize() @@ -56,14 +55,14 @@ class PickingPass(RenderPass): py = (0.5 + y / 2.0) * window_size[1] if px < 0 or px > (output.width() - 1) or py < 0 or py > (output.height() - 1): - return None + return -1 distance = output.pixel(px, py) # distance in micron, from in r, g & b channels distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm return distance ## Get the world coordinates of a picked point - def getPickedPosition(self, x, y) -> Vector: + def getPickedPosition(self, x: int, y: int) -> Vector: distance = self.getPickedDepth(x, y) ray = self._scene.getActiveCamera().getRay(x, y) From a477b8cdc245b6f0f6572997a7c126031d100bdd Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Mar 2018 09:57:46 +0100 Subject: [PATCH 31/88] Fix malyan m180 definitions CURA-4696 --- resources/definitions/malyan_m180.def.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/definitions/malyan_m180.def.json b/resources/definitions/malyan_m180.def.json index c1a424f0c9..11b61328ed 100644 --- a/resources/definitions/malyan_m180.def.json +++ b/resources/definitions/malyan_m180.def.json @@ -38,16 +38,16 @@ "machine_max_feedrate_z": { "default_value": 400 }, - "steps_per_mm_x": { + "machine_steps_per_mm_x": { "default_value": 93 }, - "steps_per_mm_y": { + "machine_steps_per_mm_y": { "default_value": 93 }, - "steps_per_mm_z": { + "machine_steps_per_mm_z": { "default_value": 1600 }, - "steps_per_mm_e": { + "machine_steps_per_mm_e": { "default_value": 92 }, "gantry_height": { From 0155a20994daedf9acf12080ba82a0c879e2ca23 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 15 Mar 2018 10:33:06 +0100 Subject: [PATCH 32/88] CURA-4870 Fix visibility in the status icon --- resources/qml/MachineSelection.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/MachineSelection.qml b/resources/qml/MachineSelection.qml index 357e7870a7..b959e20bb7 100644 --- a/resources/qml/MachineSelection.qml +++ b/resources/qml/MachineSelection.qml @@ -55,7 +55,7 @@ ToolButton PrinterStatusIcon { id: printerStatusIcon - visible: printerConnected + visible: printerConnected || isNetworkPrinter status: printerStatus anchors { From 496c8f2f7911af896de05728711f2f9ae25a1e92 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Mar 2018 10:43:05 +0100 Subject: [PATCH 33/88] Cleanup MachineManagementModel --- .../Machines/Models/MachineManagementModel.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/cura/Machines/Models/MachineManagementModel.py b/cura/Machines/Models/MachineManagementModel.py index 481a692675..7dc51f07f7 100644 --- a/cura/Machines/Models/MachineManagementModel.py +++ b/cura/Machines/Models/MachineManagementModel.py @@ -3,7 +3,7 @@ from UM.Qt.ListModel import ListModel -from PyQt5.QtCore import pyqtSlot, pyqtProperty, Qt, pyqtSignal +from PyQt5.QtCore import Qt from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerStack import ContainerStack @@ -11,6 +11,7 @@ from UM.Settings.ContainerStack import ContainerStack from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") + # # This the QML model for the quality management page. # @@ -39,7 +40,7 @@ class MachineManagementModel(ListModel): ## Handler for container added/removed events from registry def _onContainerChanged(self, container): # We only need to update when the added / removed container is a stack. - if isinstance(container, ContainerStack): + if isinstance(container, ContainerStack) and container.getMetaDataEntry("type") == "machine": self._update() ## Private convenience function to reset & repopulate the model. @@ -47,7 +48,9 @@ class MachineManagementModel(ListModel): items = [] # Get first the network enabled printers - network_filter_printers = {"type": "machine", "um_network_key": "*", "hidden": "False"} + network_filter_printers = {"type": "machine", + "um_network_key": "*", + "hidden": "False"} self._network_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**network_filter_printers) self._network_container_stacks.sort(key = lambda i: i.getMetaDataEntry("connect_group_name")) @@ -57,11 +60,11 @@ class MachineManagementModel(ListModel): metadata["definition_name"] = container.getBottom().getName() items.append({"name": metadata["connect_group_name"], - "id": container.getId(), - "metadata": metadata, - "group": catalog.i18nc("@info:title", "Network enabled printers")}) + "id": container.getId(), + "metadata": metadata, + "group": catalog.i18nc("@info:title", "Network enabled printers")}) - # Get now the local printes + # Get now the local printers local_filter_printers = {"type": "machine", "um_network_key": None} self._local_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**local_filter_printers) self._local_container_stacks.sort(key = lambda i: i.getName()) @@ -72,8 +75,8 @@ class MachineManagementModel(ListModel): metadata["definition_name"] = container.getBottom().getName() items.append({"name": container.getName(), - "id": container.getId(), - "metadata": metadata, - "group": catalog.i18nc("@info:title", "Local printers")}) + "id": container.getId(), + "metadata": metadata, + "group": catalog.i18nc("@info:title", "Local printers")}) self.setItems(items) From 15ff4045bf6957cd0b38b55dc7a09000165c7613 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 15 Mar 2018 11:02:29 +0100 Subject: [PATCH 34/88] CURA-4870 When removing a network connected printer, also remove all the other machines that were (possibly) created in the background so that there is no orphan containers. --- cura/Settings/MachineManager.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index c79d352dcb..100c7c3c31 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -470,13 +470,13 @@ class MachineManager(QObject): @pyqtProperty(str, notify = outputDevicesChanged) def activeMachineNetworkKey(self) -> str: if self._global_container_stack: - return self._global_container_stack.getMetaDataEntry("um_network_key") + return self._global_container_stack.getMetaDataEntry("um_network_key", "") return "" @pyqtProperty(str, notify = outputDevicesChanged) def activeMachineNetworkGroupName(self) -> str: if self._global_container_stack: - return self._global_container_stack.getMetaDataEntry("connect_group_name") + return self._global_container_stack.getMetaDataEntry("connect_group_name", "") return "" @pyqtProperty(QObject, notify = globalContainerChanged) @@ -662,12 +662,22 @@ class MachineManager(QObject): if other_machine_stacks: self.setActiveMachine(other_machine_stacks[0]["id"]) + metadata = ContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0] + network_key = metadata["um_network_key"] if "um_network_key" in metadata else None ExtruderManager.getInstance().removeMachineExtruders(machine_id) containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id) for container in containers: ContainerRegistry.getInstance().removeContainer(container["id"]) ContainerRegistry.getInstance().removeContainer(machine_id) + # If the printer that is being removed is a network printer, the hidden printers have to be also removed + if network_key: + metadata_filter = {"um_network_key": network_key} + hidden_containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) + if hidden_containers: + # This reuses the method and remove all printers recursively + self.removeMachine(hidden_containers[0].getId()) + @pyqtProperty(bool, notify = globalContainerChanged) def hasMaterials(self) -> bool: if self._global_container_stack: From dee70f35f4f3a228d00c3a40a6dd643693f3a1bd Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 11:03:27 +0100 Subject: [PATCH 35/88] Fix setting visibility when searching --- .../PerObjectSettingsPanel.qml | 48 ++++++++++--------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml index 7872908ee8..a2790dcf08 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml @@ -384,7 +384,6 @@ Item { title: catalog.i18nc("@title:window", "Select Settings to Customize for this model") width: screenScaleFactor * 360 - property string labelFilter: "" property var additional_excluded_settings onVisibilityChanged: @@ -395,11 +394,33 @@ Item { // Set skip setting, it will prevent from resetting selected mesh_type contents.model.visibilityHandler.addSkipResetSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) listview.model.forceUpdate() + + updateFilter() } } + function updateFilter() + { + var new_filter = {}; + if (printSequencePropertyProvider.properties.value == "one_at_a_time") + { + new_filter["settable_per_meshgroup"] = true; + } + else + { + new_filter["settable_per_mesh"] = true; + } + + if(filterInput.text != "") + { + new_filter["i18n_label"] = "*" + filterInput.text; + } + + listview.model.filter = new_filter; + } + TextField { - id: filter + id: filterInput anchors { top: parent.top @@ -410,17 +431,7 @@ Item { placeholderText: catalog.i18nc("@label:textbox", "Filter..."); - onTextChanged: - { - if(text != "") - { - listview.model.filter = {"settable_per_mesh": true, "i18n_label": "*" + text} - } - else - { - listview.model.filter = {"settable_per_mesh": true} - } - } + onTextChanged: settingPickDialog.updateFilter() } CheckBox @@ -446,7 +457,7 @@ Item { anchors { - top: filter.bottom; + top: filterInput.bottom; left: parent.left; right: parent.right; bottom: parent.bottom; @@ -458,14 +469,6 @@ Item { { id: definitionsModel; containerId: Cura.MachineManager.activeDefinitionId - filter: - { - if (printSequencePropertyProvider.properties.value == "one_at_a_time") - { - return {"settable_per_meshgroup": true}; - } - return {"settable_per_mesh": true}; - } visibilityHandler: UM.SettingPreferenceVisibilityHandler {} expanded: [ "*" ] exclude: @@ -497,6 +500,7 @@ Item { } } } + Component.onCompleted: settingPickDialog.updateFilter() } } From 584f98cb071cc140d4fa9dd299f5820796d27a50 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 11:27:48 +0100 Subject: [PATCH 36/88] Fix code style --- cura/PrinterOutput/ExtruderOutputModel.py | 3 ++- cura/PrinterOutput/GenericOutputController.py | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cura/PrinterOutput/ExtruderOutputModel.py b/cura/PrinterOutput/ExtruderOutputModel.py index 92b3ad6d7c..75b9cc98ac 100644 --- a/cura/PrinterOutput/ExtruderOutputModel.py +++ b/cura/PrinterOutput/ExtruderOutputModel.py @@ -20,7 +20,7 @@ class ExtruderOutputModel(QObject): extruderConfigurationChanged = pyqtSignal() isPreheatingChanged = pyqtSignal() - def __init__(self, printer: "PrinterOutputModel", position: int, parent=None): + def __init__(self, printer: "PrinterOutputModel", position, parent=None): super().__init__(parent) self._printer = printer self._position = position @@ -98,6 +98,7 @@ class ExtruderOutputModel(QObject): if self._extruder_configuration.isValid(): return self._extruder_configuration return None + def updateIsPreheating(self, pre_heating): if self._is_preheating != pre_heating: self._is_preheating = pre_heating diff --git a/cura/PrinterOutput/GenericOutputController.py b/cura/PrinterOutput/GenericOutputController.py index 106c1e3a44..a21425af92 100644 --- a/cura/PrinterOutput/GenericOutputController.py +++ b/cura/PrinterOutput/GenericOutputController.py @@ -52,7 +52,6 @@ class GenericOutputController(PrinterOutputController): extruder.updateIsPreheating(False) self._preheat_hotends = set() - def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): self._output_device.sendCommand("G91") self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) @@ -76,7 +75,6 @@ class GenericOutputController(PrinterOutputController): self._output_device.cancelPrint() pass - def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): self._output_device.sendCommand("M140 S%s" % temperature) @@ -107,7 +105,6 @@ class GenericOutputController(PrinterOutputController): self.setTargetBedTemperature(self._preheat_printer, 0) self._preheat_printer.updateIsPreheating(False) - def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: int): self._output_device.sendCommand("M104 S%s T%s" % (temperature, position)) From c25711797e6ccb9d7924ea7c724c5a40529dcf9f Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 12:46:22 +0100 Subject: [PATCH 37/88] Click support eraser mesh to remove it from the scene --- plugins/SupportEraser/SupportEraser.py | 27 +++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 65d22bcdfd..bbb3fefca5 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -7,6 +7,7 @@ from UM.Application import Application from UM.Event import Event, MouseEvent from UM.Mesh.MeshBuilder import MeshBuilder from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation +from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Settings.SettingInstance import SettingInstance from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator @@ -24,24 +25,39 @@ class SupportEraser(Tool): self._shortcut_key = Qt.Key_G self._controller = Application.getInstance().getController() + self._selection_pass = None Application.getInstance().globalContainerStackChanged.connect(self._updateEnabled) def event(self, event): super().event(event) if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): - active_camera = self._controller.getScene().getActiveCamera() + + if self._selection_pass is None: + # The selection renderpass is used to identify objects in the current view + self._selection_pass = Application.getInstance().getRenderer().getRenderPass("selection") + picked_node = self._controller.getScene().findObject(self._selection_pass.getIdAtPosition(event.x, event.y)) + + node_stack = picked_node.callDecoration("getStack") + if node_stack: + if node_stack.getProperty("anti_overhang_mesh", "value"): + self._removeEraserMesh(picked_node) + return + + elif node_stack.getProperty("support_mesh", "value") or node_stack.getProperty("infill_mesh", "value") or node_stack.getProperty("cutting_mesh", "value"): + return # Create a pass for picking a world-space location from the mouse location + active_camera = self._controller.getScene().getActiveCamera() picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight()) picking_pass.render() picked_position = picking_pass.getPickedPosition(event.x, event.y) # Add the anti_overhang_mesh cube at the picked location - self._createEraserMesh(picked_position) + self._createEraserMesh(picked_node, picked_position) - def _createEraserMesh(self, position: Vector): + def _createEraserMesh(self, parent: CuraSceneNode, position: Vector): node = CuraSceneNode() node.setName("Eraser") @@ -76,6 +92,11 @@ class SupportEraser(Tool): op.push() Application.getInstance().getController().getScene().sceneChanged.emit(node) + def _removeEraserMesh(self, node: CuraSceneNode): + op = RemoveSceneNodeOperation(node) + op.push() + Application.getInstance().getController().getScene().sceneChanged.emit(node) + def _updateEnabled(self): plugin_enabled = False From a0c44192da1f1455d7ba8b564d95676d369cdb17 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 14:17:59 +0100 Subject: [PATCH 38/88] Add support eraser meshes to group so it does not drop --- plugins/SupportEraser/SupportEraser.py | 82 +++++++++++++++++--------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index bbb3fefca5..8f8e9deb52 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -1,24 +1,35 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from UM.Math.Vector import Vector -from UM.Tool import Tool -from PyQt5.QtCore import Qt, QUrl -from UM.Application import Application -from UM.Event import Event, MouseEvent -from UM.Mesh.MeshBuilder import MeshBuilder -from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation -from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation -from UM.Settings.SettingInstance import SettingInstance -from cura.Scene.CuraSceneNode import CuraSceneNode -from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator -from cura.Scene.BuildPlateDecorator import BuildPlateDecorator -from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator -from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -from cura.PickingPass import PickingPass import os import os.path +from PyQt5.QtCore import Qt, QUrl + +from UM.Math.Vector import Vector +from UM.Tool import Tool +from UM.Application import Application +from UM.Event import Event, MouseEvent + +from UM.Mesh.MeshBuilder import MeshBuilder +from UM.Scene.Selection import Selection +from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator +from cura.Scene.CuraSceneNode import CuraSceneNode + +from cura.PickingPass import PickingPass + +from UM.Operations.GroupedOperation import GroupedOperation +from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation +from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation +from cura.Operations.SetParentOperation import SetParentOperation + +from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator +from cura.Scene.BuildPlateDecorator import BuildPlateDecorator +from UM.Scene.GroupDecorator import GroupDecorator +from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator + +from UM.Settings.SettingInstance import SettingInstance + class SupportEraser(Tool): def __init__(self): super().__init__() @@ -45,6 +56,7 @@ class SupportEraser(Tool): return elif node_stack.getProperty("support_mesh", "value") or node_stack.getProperty("infill_mesh", "value") or node_stack.getProperty("cutting_mesh", "value"): + # Only "normal" meshes can have anti_overhang_meshes added to them return # Create a pass for picking a world-space location from the mouse location @@ -73,22 +85,36 @@ class SupportEraser(Tool): node.addDecorator(BuildPlateDecorator(active_build_plate)) node.addDecorator(SliceableObjectDecorator()) - stack = node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway. - if not stack: - node.addDecorator(SettingOverrideDecorator()) - stack = node.callDecoration("getStack") - + stack = node.callDecoration("getStack") # created by SettingOverrideDecorator settings = stack.getTop() - if not (settings.getInstance("anti_overhang_mesh") and settings.getProperty("anti_overhang_mesh", "value")): - definition = stack.getSettingDefinition("anti_overhang_mesh") - new_instance = SettingInstance(definition, settings) - new_instance.setProperty("value", True) - new_instance.resetState() # Ensure that the state is not seen as a user state. - settings.addInstance(new_instance) + definition = stack.getSettingDefinition("anti_overhang_mesh") + new_instance = SettingInstance(definition, settings) + new_instance.setProperty("value", True) + new_instance.resetState() # Ensure that the state is not seen as a user state. + settings.addInstance(new_instance) - scene = self._controller.getScene() - op = AddSceneNodeOperation(node, scene.getRoot()) + root = self._controller.getScene().getRoot() + + op = GroupedOperation() + # First add the node to the scene, so it gets the expected transform + op.addOperation(AddSceneNodeOperation(node, root)) + + # Determine the parent group the node should be put in + if parent.getParent().callDecoration("isGroup"): + group = parent.getParent() + else: + # Create a group-node + group = CuraSceneNode() + group.addDecorator(GroupDecorator()) + group.addDecorator(BuildPlateDecorator(active_build_plate)) + group.setParent(root) + center = parent.getPosition() + group.setPosition(center) + group.setCenterPosition(center) + op.addOperation(SetParentOperation(parent, group)) + + op.addOperation(SetParentOperation(node, group)) op.push() Application.getInstance().getController().getScene().sceneChanged.emit(node) From 097c97b6f8640df7151fa47c5ca5e0e591e803a8 Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Thu, 15 Mar 2018 14:33:27 +0100 Subject: [PATCH 39/88] Fix: Refresh list of available printers in network after clicking refresh button. --- plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py index 5ff5eb9e3e..089b9038f7 100644 --- a/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py @@ -82,6 +82,9 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._zero_conf_browser.cancel() self._zero_conf_browser = None # Force the old ServiceBrowser to be destroyed. + for instance_name in list(self._discovered_devices): + self._onRemoveDevice(instance_name) + self._zero_conf = Zeroconf() self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._appendServiceChangedRequest]) From 1f2602a2f4c32447b421d7de4e43b59544639361 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Mar 2018 10:41:50 +0100 Subject: [PATCH 40/88] Move SettingVisibilityPresetsModel to Machines.Models CURA-5088 --- cura/CuraApplication.py | 6 +----- .../Models}/SettingVisibilityPresetsModel.py | 0 2 files changed, 1 insertion(+), 5 deletions(-) rename cura/{Settings => Machines/Models}/SettingVisibilityPresetsModel.py (100%) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index b7731c5c8c..243bb2eb8a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1,9 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -#Type hinting. -from typing import Dict - from PyQt5.QtCore import QObject, QTimer from PyQt5.QtNetwork import QLocalServer from PyQt5.QtNetwork import QLocalSocket @@ -91,7 +88,7 @@ from cura.Settings.UserChangesModel import UserChangesModel from cura.Settings.ExtrudersModel import ExtrudersModel from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler from cura.Settings.ContainerManager import ContainerManager -from cura.Settings.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel +from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel from cura.ObjectsModel import ObjectsModel @@ -101,7 +98,6 @@ from PyQt5.QtGui import QColor, QIcon from PyQt5.QtWidgets import QMessageBox from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType -from configparser import ConfigParser import sys import os.path import numpy diff --git a/cura/Settings/SettingVisibilityPresetsModel.py b/cura/Machines/Models/SettingVisibilityPresetsModel.py similarity index 100% rename from cura/Settings/SettingVisibilityPresetsModel.py rename to cura/Machines/Models/SettingVisibilityPresetsModel.py From 83175b00c282a6f63943522ff70e7447775b74f1 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Mar 2018 10:59:23 +0100 Subject: [PATCH 41/88] Change SettingVisibilityPresetsModel to non-singleton CURA-5088 --- cura/CuraApplication.py | 19 +++++++++++++------ .../Models/SettingVisibilityPresetsModel.py | 16 ---------------- .../Menus/SettingVisibilityPresetsMenu.qml | 11 ++++++----- .../qml/Preferences/SettingVisibilityPage.qml | 17 +++++++++-------- resources/qml/Settings/SettingView.qml | 11 ++++++----- 5 files changed, 34 insertions(+), 40 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 243bb2eb8a..1b5de89c2b 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -65,6 +65,8 @@ from cura.Machines.Models.QualityManagementModel import QualityManagementModel from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel from cura.Machines.Models.MachineManagementModel import MachineManagementModel +from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel + from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Settings.SettingInheritanceManager import SettingInheritanceManager @@ -88,7 +90,6 @@ from cura.Settings.UserChangesModel import UserChangesModel from cura.Settings.ExtrudersModel import ExtrudersModel from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler from cura.Settings.ContainerManager import ContainerManager -from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel from cura.ObjectsModel import ObjectsModel @@ -222,6 +223,7 @@ class CuraApplication(QtApplication): self._object_manager = None self._build_plate_model = None self._multi_build_plate_model = None + self._setting_visibility_presets_model = None self._setting_inheritance_manager = None self._simple_mode_settings_manager = None self._cura_scene_controller = None @@ -377,10 +379,6 @@ class CuraApplication(QtApplication): preferences.setDefault("local_file/last_used_type", "text/x-gcode") - default_visibility_profile = SettingVisibilityPresetsModel.getInstance().getItem(0) - - preferences.setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"])) - self.applicationShuttingDown.connect(self.saveSettings) self.engineCreatedSignal.connect(self._onEngineCreated) @@ -683,6 +681,11 @@ class CuraApplication(QtApplication): self._print_information = PrintInformation.PrintInformation() self._cura_actions = CuraActions.CuraActions(self) + # Initialize setting visibility presets model + self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self) + default_visibility_profile = self._setting_visibility_presets_model.getItem(0) + Preferences.getInstance().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"])) + # Detect in which mode to run and execute that mode if self.getCommandLineOption("headless", False): self.runWithoutGUI() @@ -765,6 +768,10 @@ class CuraApplication(QtApplication): def hasGui(self): return self._use_gui + @pyqtSlot(result = QObject) + def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel: + return self._setting_visibility_presets_model + def getMachineErrorChecker(self, *args) -> MachineErrorChecker: return self._machine_error_checker @@ -891,11 +898,11 @@ class CuraApplication(QtApplication): qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel") qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler") + qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel") qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel") qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator") qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel") qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager) - qmlRegisterSingletonType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel", SettingVisibilityPresetsModel.createSettingVisibilityPresetsModel) # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work. actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml"))) diff --git a/cura/Machines/Models/SettingVisibilityPresetsModel.py b/cura/Machines/Models/SettingVisibilityPresetsModel.py index e5a2e24412..ace78ae093 100644 --- a/cura/Machines/Models/SettingVisibilityPresetsModel.py +++ b/cura/Machines/Models/SettingVisibilityPresetsModel.py @@ -118,19 +118,3 @@ class SettingVisibilityPresetsModel(ListModel): # Copy current visibility set to custom visibility set preference so it can be restored later visibility_string = self._preferences.getValue("general/visible_settings") self._preferences.setValue("cura/custom_visible_settings", visibility_string) - - - # Factory function, used by QML - @staticmethod - def createSettingVisibilityPresetsModel(engine, js_engine): - return SettingVisibilityPresetsModel.getInstance() - - ## Get the singleton instance for this class. - @classmethod - def getInstance(cls) -> "SettingVisibilityPresetsModel": - # Note: Explicit use of class name to prevent issues with inheritance. - if not SettingVisibilityPresetsModel.__instance: - SettingVisibilityPresetsModel.__instance = cls() - return SettingVisibilityPresetsModel.__instance - - __instance = None # type: "SettingVisibilityPresetsModel" \ No newline at end of file diff --git a/resources/qml/Menus/SettingVisibilityPresetsMenu.qml b/resources/qml/Menus/SettingVisibilityPresetsMenu.qml index 19c36e6118..d39d65e96c 100644 --- a/resources/qml/Menus/SettingVisibilityPresetsMenu.qml +++ b/resources/qml/Menus/SettingVisibilityPresetsMenu.qml @@ -12,6 +12,7 @@ Menu id: menu title: catalog.i18nc("@action:inmenu", "Visible Settings") + property QtObject settingVisibilityPresetsModel: CuraApplication.getSettingVisibilityPresetsModel() property bool showingSearchResults property bool showingAllSettings @@ -22,11 +23,11 @@ Menu { text: catalog.i18nc("@action:inmenu", "Custom selection") checkable: true - checked: !showingSearchResults && !showingAllSettings && Cura.SettingVisibilityPresetsModel.activePreset == "custom" + checked: !showingSearchResults && !showingAllSettings && settingVisibilityPresetsModel.activePreset == "custom" exclusiveGroup: group onTriggered: { - Cura.SettingVisibilityPresetsModel.setActivePreset("custom"); + settingVisibilityPresetsModel.setActivePreset("custom"); // Restore custom set from preference UM.Preferences.setValue("general/visible_settings", UM.Preferences.getValue("cura/custom_visible_settings")); showSettingVisibilityProfile(); @@ -36,17 +37,17 @@ Menu Instantiator { - model: Cura.SettingVisibilityPresetsModel + model: settingVisibilityPresetsModel MenuItem { text: model.name checkable: true - checked: model.id == Cura.SettingVisibilityPresetsModel.activePreset + checked: model.id == settingVisibilityPresetsModel.activePreset exclusiveGroup: group onTriggered: { - Cura.SettingVisibilityPresetsModel.setActivePreset(model.id); + settingVisibilityPresetsModel.setActivePreset(model.id); UM.Preferences.setValue("general/visible_settings", model.settings.join(";")); diff --git a/resources/qml/Preferences/SettingVisibilityPage.qml b/resources/qml/Preferences/SettingVisibilityPage.qml index f0c24e2cbe..7f6a58367d 100644 --- a/resources/qml/Preferences/SettingVisibilityPage.qml +++ b/resources/qml/Preferences/SettingVisibilityPage.qml @@ -13,6 +13,8 @@ UM.PreferencesPage { title: catalog.i18nc("@title:tab", "Setting Visibility"); + property QtObject settingVisibilityPresetsModel: CuraApplication.getSettingVisibilityPresetsModel() + property int scrollToIndex: 0 signal scrollToSection( string key ) @@ -132,10 +134,9 @@ UM.PreferencesPage { visibilityPresetsModel.append({text: catalog.i18nc("@action:inmenu", "Custom selection"), id: "custom"}); - var presets = Cura.SettingVisibilityPresetsModel; - for(var i = 0; i < presets.rowCount(); i++) + for(var i = 0; i < settingVisibilityPresetsModel.rowCount(); i++) { - visibilityPresetsModel.append({text: presets.getItem(i)["name"], id: presets.getItem(i)["id"]}); + visibilityPresetsModel.append({text: settingVisibilityPresetsModel.getItem(i)["name"], id: settingVisibilityPresetsModel.getItem(i)["id"]}); } } } @@ -143,7 +144,7 @@ UM.PreferencesPage currentIndex: { // Load previously selected preset. - var index = Cura.SettingVisibilityPresetsModel.find("id", Cura.SettingVisibilityPresetsModel.activePreset); + var index = settingVisibilityPresetsModel.find("id", settingVisibilityPresetsModel.activePreset); if(index == -1) { return 0; @@ -156,12 +157,12 @@ UM.PreferencesPage { base.inhibitSwitchToCustom = true; var preset_id = visibilityPresetsModel.get(index).id; - Cura.SettingVisibilityPresetsModel.setActivePreset(preset_id); + settingVisibilityPresetsModel.setActivePreset(preset_id); UM.Preferences.setValue("cura/active_setting_visibility_preset", preset_id); if (preset_id != "custom") { - UM.Preferences.setValue("general/visible_settings", Cura.SettingVisibilityPresetsModel.getItem(index - 1).settings.join(";")); + UM.Preferences.setValue("general/visible_settings", settingVisibilityPresetsModel.getItem(index - 1).settings.join(";")); // "Custom selection" entry is added in front, so index is off by 1 } else @@ -203,9 +204,9 @@ UM.PreferencesPage { onVisibilityChanged: { - if(Cura.SettingVisibilityPresetsModel.activePreset != "" && !base.inhibitSwitchToCustom) + if(settingVisibilityPresetsModel.activePreset != "" && !base.inhibitSwitchToCustom) { - Cura.SettingVisibilityPresetsModel.setActivePreset("custom"); + settingVisibilityPresetsModel.setActivePreset("custom"); } } } diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index 235dfac91a..a6d7b3a71e 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -15,6 +15,7 @@ Item { id: base; + property QtObject settingVisibilityPresetsModel: CuraApplication.getSettingVisibilityPresetsModel() property Action configureSettings property bool findingSettings property bool showingAllSettings @@ -562,9 +563,9 @@ Item { definitionsModel.hide(contextMenu.key); // visible settings have changed, so we're no longer showing a preset - if (Cura.SettingVisibilityPresetsModel.activePreset != "" && !showingAllSettings) + if (settingVisibilityPresetsModel.activePreset != "" && !showingAllSettings) { - Cura.SettingVisibilityPresetsModel.setActivePreset("custom"); + settingVisibilityPresetsModel.setActivePreset("custom"); } } } @@ -594,16 +595,16 @@ Item definitionsModel.show(contextMenu.key); } // visible settings have changed, so we're no longer showing a preset - if (Cura.SettingVisibilityPresetsModel.activePreset != "" && !showingAllSettings) + if (settingVisibilityPresetsModel.activePreset != "" && !showingAllSettings) { - Cura.SettingVisibilityPresetsModel.setActivePreset("custom"); + settingVisibilityPresetsModel.setActivePreset("custom"); } } } MenuItem { //: Settings context menu action - text: catalog.i18nc("@action:menu", "Configure setting visiblity..."); + text: catalog.i18nc("@action:menu", "Configure setting visibility..."); onTriggered: Cura.Actions.configureSettingVisibility.trigger(contextMenu); } From 8e39849aadd34fcd93a37dffbdae837bd700ca21 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Mar 2018 12:04:14 +0100 Subject: [PATCH 42/88] Refactor setting visibility preset CURA-5088 --- .../Models/SettingVisibilityPresetsModel.py | 140 ++++++++++++------ .../Menus/SettingVisibilityPresetsMenu.qml | 23 +-- .../qml/Preferences/SettingVisibilityPage.qml | 60 ++------ 3 files changed, 109 insertions(+), 114 deletions(-) diff --git a/cura/Machines/Models/SettingVisibilityPresetsModel.py b/cura/Machines/Models/SettingVisibilityPresetsModel.py index ace78ae093..e281d81c39 100644 --- a/cura/Machines/Models/SettingVisibilityPresetsModel.py +++ b/cura/Machines/Models/SettingVisibilityPresetsModel.py @@ -1,11 +1,12 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Optional import os -import urllib +import urllib.parse from configparser import ConfigParser -from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot, QUrl +from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot from UM.Logger import Logger from UM.Qt.ListModel import ListModel @@ -13,13 +14,14 @@ from UM.Preferences import Preferences from UM.Resources import Resources from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError -import cura.CuraApplication +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") class SettingVisibilityPresetsModel(ListModel): IdRole = Qt.UserRole + 1 NameRole = Qt.UserRole + 2 - SettingsRole = Qt.UserRole + 4 + SettingsRole = Qt.UserRole + 3 def __init__(self, parent = None): super().__init__(parent) @@ -28,39 +30,51 @@ class SettingVisibilityPresetsModel(ListModel): self.addRoleName(self.SettingsRole, "settings") self._populate() + basic_item = self.items[1] + basic_visibile_settings = ";".join(basic_item["settings"]) self._preferences = Preferences.getInstance() - self._preferences.addPreference("cura/active_setting_visibility_preset", "custom") # Preference to store which preset is currently selected - self._preferences.addPreference("cura/custom_visible_settings", "") # Preference that stores the "custom" set so it can always be restored (even after a restart) + # Preference to store which preset is currently selected + self._preferences.addPreference("cura/active_setting_visibility_preset", "basic") + # Preference that stores the "custom" set so it can always be restored (even after a restart) + self._preferences.addPreference("cura/custom_visible_settings", basic_visibile_settings) self._preferences.preferenceChanged.connect(self._onPreferencesChanged) - self._active_preset = self._preferences.getValue("cura/active_setting_visibility_preset") - if self.find("id", self._active_preset) < 0: - self._active_preset = "custom" + self._active_preset_item = self._getItem(self._preferences.getValue("cura/active_setting_visibility_preset")) + # Initialize visible settings if it is not done yet + visible_settings = self._preferences.getValue("general/visible_settings") + if not visible_settings: + self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item["settings"])) self.activePresetChanged.emit() + def _getItem(self, item_id: str) -> Optional[dict]: + result = None + for item in self.items: + if item["id"] == item_id: + result = item + break + return result def _populate(self): + from cura.CuraApplication import CuraApplication items = [] - for item in Resources.getAllResourcesOfType(cura.CuraApplication.CuraApplication.ResourceTypes.SettingVisibilityPreset): + for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset): try: - mime_type = MimeTypeDatabase.getMimeTypeForFile(item) + mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path) except MimeTypeNotFoundError: - Logger.log("e", "Could not determine mime type of file %s", item) + Logger.log("e", "Could not determine mime type of file %s", file_path) continue - id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(item))) - - if not os.path.isfile(item): + item_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_path))) + if not os.path.isfile(file_path): + Logger.log("e", "[%s] is not a file", file_path) continue - parser = ConfigParser(allow_no_value=True) # accept options without any value, - + parser = ConfigParser(allow_no_value = True) # accept options without any value, try: - parser.read([item]) - - if not parser.has_option("general", "name") and not parser.has_option("general", "weight"): + parser.read([file_path]) + if not parser.has_option("general", "name") or not parser.has_option("general", "weight"): continue settings = [] @@ -73,48 +87,90 @@ class SettingVisibilityPresetsModel(ListModel): settings.append(option) items.append({ - "id": id, - "name": parser["general"]["name"], + "id": item_id, + "name": catalog.i18nc("@action:inmenu", parser["general"]["name"]), "weight": parser["general"]["weight"], - "settings": settings + "settings": settings, }) - except Exception as e: - Logger.log("e", "Failed to load setting preset %s: %s", file_path, str(e)) + except Exception: + Logger.logException("e", "Failed to load setting preset %s", file_path) + items.sort(key = lambda k: (int(k["weight"]), k["id"])) + # Put "custom" at the top + items.insert(0, {"id": "custom", + "name": "Custom selection", + "weight": -100, + "settings": []}) - items.sort(key = lambda k: (k["weight"], k["id"])) self.setItems(items) @pyqtSlot(str) - def setActivePreset(self, preset_id): - if preset_id != "custom" and self.find("id", preset_id) == -1: - Logger.log("w", "Tried to set active preset to unknown id %s", preset_id) + def setActivePreset(self, preset_id: str): + if preset_id == self._active_preset_item["id"]: + Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id) return - if preset_id == "custom" and self._active_preset == "custom": - # Copy current visibility set to custom visibility set preference so it can be restored later - visibility_string = self._preferences.getValue("general/visible_settings") - self._preferences.setValue("cura/custom_visible_settings", visibility_string) + preset_item = None + for item in self.items: + if item["id"] == preset_id: + preset_item = item + break + if preset_item is None: + Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id) + return + + need_to_save_to_custom = self._active_preset_item["id"] == "custom" and preset_id != "custom" + if need_to_save_to_custom: + # Save the current visibility settings to custom + current_visibility_string = self._preferences.getValue("general/visible_settings") + if current_visibility_string: + self._preferences.setValue("cura/custom_visible_settings", current_visibility_string) + + new_visibility_string = ";".join(preset_item["settings"]) + if preset_id == "custom": + # Get settings from the stored custom data + new_visibility_string = self._preferences.getValue("cura/custom_visible_settings") + if new_visibility_string is None: + new_visibility_string = self._preferences.getValue("general/visible_settings") + self._preferences.setValue("general/visible_settings", new_visibility_string) self._preferences.setValue("cura/active_setting_visibility_preset", preset_id) - - self._active_preset = preset_id + self._active_preset_item = preset_item self.activePresetChanged.emit() activePresetChanged = pyqtSignal() @pyqtProperty(str, notify = activePresetChanged) - def activePreset(self): - return self._active_preset + def activePreset(self) -> str: + return self._active_preset_item["id"] - def _onPreferencesChanged(self, name): + def _onPreferencesChanged(self, name: str): if name != "general/visible_settings": return - if self._active_preset != "custom": + # Find the preset that matches with the current visible settings setup + visibility_string = self._preferences.getValue("general/visible_settings") + if not visibility_string: return - # Copy current visibility set to custom visibility set preference so it can be restored later - visibility_string = self._preferences.getValue("general/visible_settings") - self._preferences.setValue("cura/custom_visible_settings", visibility_string) + visibility_set = set(visibility_string.split(";")) + matching_preset_item = None + for item in self.items: + if item["id"] == "custom": + continue + if set(item["settings"]) == visibility_set: + matching_preset_item = item + break + + if matching_preset_item is None: + # The new visibility setup is "custom" should be custom + if self._active_preset_item["id"] == "custom": + # We are already in custom, just save the settings + self._preferences.setValue("cura/custom_visible_settings", visibility_string) + else: + self._active_preset_item = self.items[0] # 0 is custom + self.activePresetChanged.emit() + else: + self._active_preset_item = matching_preset_item + self.activePresetChanged.emit() diff --git a/resources/qml/Menus/SettingVisibilityPresetsMenu.qml b/resources/qml/Menus/SettingVisibilityPresetsMenu.qml index d39d65e96c..0753c83b17 100644 --- a/resources/qml/Menus/SettingVisibilityPresetsMenu.qml +++ b/resources/qml/Menus/SettingVisibilityPresetsMenu.qml @@ -1,8 +1,8 @@ // Copyright (c) 2018 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. -import QtQuick 2.2 -import QtQuick.Controls 1.1 +import QtQuick 2.7 +import QtQuick.Controls 1.4 import UM 1.2 as UM import Cura 1.0 as Cura @@ -19,22 +19,6 @@ Menu signal showAllSettings() signal showSettingVisibilityProfile() - MenuItem - { - text: catalog.i18nc("@action:inmenu", "Custom selection") - checkable: true - checked: !showingSearchResults && !showingAllSettings && settingVisibilityPresetsModel.activePreset == "custom" - exclusiveGroup: group - onTriggered: - { - settingVisibilityPresetsModel.setActivePreset("custom"); - // Restore custom set from preference - UM.Preferences.setValue("general/visible_settings", UM.Preferences.getValue("cura/custom_visible_settings")); - showSettingVisibilityProfile(); - } - } - MenuSeparator { } - Instantiator { model: settingVisibilityPresetsModel @@ -48,9 +32,6 @@ Menu onTriggered: { settingVisibilityPresetsModel.setActivePreset(model.id); - - UM.Preferences.setValue("general/visible_settings", model.settings.join(";")); - showSettingVisibilityProfile(); } } diff --git a/resources/qml/Preferences/SettingVisibilityPage.qml b/resources/qml/Preferences/SettingVisibilityPage.qml index 7f6a58367d..b6b1c133ed 100644 --- a/resources/qml/Preferences/SettingVisibilityPage.qml +++ b/resources/qml/Preferences/SettingVisibilityPage.qml @@ -29,8 +29,7 @@ UM.PreferencesPage // After calling this function update Setting visibility preset combobox. // Reset should set default setting preset ("Basic") - visibilityPreset.setDefaultPreset() - + visibilityPreset.currentIndex = 1 } resetEnabled: true; @@ -39,8 +38,6 @@ UM.PreferencesPage id: base; anchors.fill: parent; - property bool inhibitSwitchToCustom: false - CheckBox { id: toggleVisibleSettings @@ -114,11 +111,6 @@ UM.PreferencesPage ComboBox { - function setDefaultPreset() - { - visibilityPreset.currentIndex = 0 - } - id: visibilityPreset width: 150 * screenScaleFactor anchors @@ -127,50 +119,25 @@ UM.PreferencesPage right: parent.right } - model: ListModel - { - id: visibilityPresetsModel - Component.onCompleted: - { - visibilityPresetsModel.append({text: catalog.i18nc("@action:inmenu", "Custom selection"), id: "custom"}); - - for(var i = 0; i < settingVisibilityPresetsModel.rowCount(); i++) - { - visibilityPresetsModel.append({text: settingVisibilityPresetsModel.getItem(i)["name"], id: settingVisibilityPresetsModel.getItem(i)["id"]}); - } - } - } + model: settingVisibilityPresetsModel + textRole: "name" currentIndex: { // Load previously selected preset. - var index = settingVisibilityPresetsModel.find("id", settingVisibilityPresetsModel.activePreset); - if(index == -1) + var index = settingVisibilityPresetsModel.find("id", settingVisibilityPresetsModel.activePreset) + if (index == -1) { - return 0; + return 0 } - return index + 1; // "Custom selection" entry is added in front, so index is off by 1 + return index } onActivated: { - base.inhibitSwitchToCustom = true; - var preset_id = visibilityPresetsModel.get(index).id; + var preset_id = settingVisibilityPresetsModel.getItem(index).id; settingVisibilityPresetsModel.setActivePreset(preset_id); - - UM.Preferences.setValue("cura/active_setting_visibility_preset", preset_id); - if (preset_id != "custom") - { - UM.Preferences.setValue("general/visible_settings", settingVisibilityPresetsModel.getItem(index - 1).settings.join(";")); - // "Custom selection" entry is added in front, so index is off by 1 - } - else - { - // Restore custom set from preference - UM.Preferences.setValue("general/visible_settings", UM.Preferences.getValue("cura/custom_visible_settings")); - } - base.inhibitSwitchToCustom = false; } } @@ -200,16 +167,7 @@ UM.PreferencesPage exclude: ["machine_settings", "command_line_settings"] showAncestors: true expanded: ["*"] - visibilityHandler: UM.SettingPreferenceVisibilityHandler - { - onVisibilityChanged: - { - if(settingVisibilityPresetsModel.activePreset != "" && !base.inhibitSwitchToCustom) - { - settingVisibilityPresetsModel.setActivePreset("custom"); - } - } - } + visibilityHandler: UM.SettingPreferenceVisibilityHandler {} } delegate: Loader From cbe929242ec59b227dc03079272978e1327ec786 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 15 Mar 2018 14:44:01 +0100 Subject: [PATCH 43/88] CURA-4870 Update the names of the groups in the container stacks when there is temporary name after upgrading from 3.2 --- .../NetworkedPrinterOutputDevice.py | 16 ++++++++++++++++ cura/Settings/MachineManager.py | 2 -- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index eefbd9ae12..0dca149e5a 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -3,6 +3,7 @@ from UM.Application import Application from UM.Logger import Logger +from UM.Settings.ContainerRegistry import ContainerRegistry from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState @@ -254,6 +255,21 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._last_manager_create_time = time() self._manager.authenticationRequired.connect(self._onAuthenticationRequired) + self._checkCorrectGroupName() + + ## This method checks if the name of the group stored in the definition container is correct. + # After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group + # then all the container stacks are updated, both the current and the hidden ones. + def _checkCorrectGroupName(self): + global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack and self.getId() == global_container_stack.getMetaDataEntry("um_network_key"): + # Check if the connect_group_name is correct. If not, update all the containers connected to the same printer + if global_container_stack.getMetaDataEntry("connect_group_name") != self.name: + metadata_filter = {"um_network_key": global_container_stack.getMetaDataEntry("um_network_key")} + hidden_containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) + for container in hidden_containers: + container.setMetaDataEntry("connect_group_name", self.name) + def _registerOnFinishedCallback(self, reply: QNetworkReply, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: if onFinished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 100c7c3c31..1cf3fae161 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -10,7 +10,6 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Signal import Signal from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer -import UM.FlameProfiler from UM.FlameProfiler import pyqtSlot from UM import Util @@ -24,7 +23,6 @@ from UM.Settings.SettingFunction import SettingFunction from UM.Signal import postponeSignals, CompressTechnique from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch -from cura.Machines.VariantManager import VariantType from cura.PrinterOutputDevice import PrinterOutputDevice from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel From 98b325c9d5334b5a6a0eff6c7918207c6a054d21 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Mar 2018 14:54:09 +0100 Subject: [PATCH 44/88] Fix material serialization crash --- plugins/XmlMaterialProfile/XmlMaterialProfile.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 8b17721794..5ff6838373 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -208,14 +208,9 @@ class XmlMaterialProfile(InstanceContainer): machine_variant_map = {} variant_manager = CuraApplication.getInstance().getVariantManager() - material_manager = CuraApplication.getInstance().getMaterialManager() root_material_id = self.getMetaDataEntry("base_file") # if basefile is self.getId, this is a basefile. - material_group = material_manager.getMaterialGroup(root_material_id) - - all_containers = [] - for node in [material_group.root_material_node] + material_group.derived_material_node_list: - all_containers.append(node.getContainer()) + all_containers = registry.findInstanceContainers(base_file = root_material_id) for container in all_containers: definition_id = container.getMetaDataEntry("definition") @@ -242,7 +237,7 @@ class XmlMaterialProfile(InstanceContainer): for definition_id, container in machine_container_map.items(): definition_id = container.getMetaDataEntry("definition") - definition_metadata = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = definition_id)[0] + definition_metadata = registry.findDefinitionContainersMetadata(id = definition_id)[0] product = definition_id for product_name, product_id_list in product_id_map.items(): From c3c096aaa015936441d8e7360c427f5af67c18aa Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 14:55:15 +0100 Subject: [PATCH 45/88] Select the picked node so the group does not get drawn --- plugins/SupportEraser/SupportEraser.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 8f8e9deb52..d87a887d1b 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -118,6 +118,12 @@ class SupportEraser(Tool): op.push() Application.getInstance().getController().getScene().sceneChanged.emit(node) + # Select the picked node so the group does not get drawn as a wireframe (yet) + if Selection.isSelected(group): + Selection.remove(group) + if not Selection.isSelected(parent): + Selection.add(parent) + def _removeEraserMesh(self, node: CuraSceneNode): op = RemoveSceneNodeOperation(node) op.push() From 05cd937df35335744524cd5268f089a1296ebf0c Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 15 Mar 2018 15:00:13 +0100 Subject: [PATCH 46/88] CURA-4400 optional_extruders cannot be set to disabled extruders anymore --- cura/Settings/ExtrudersModel.py | 1 + resources/qml/Settings/SettingExtruder.qml | 3 ++- .../qml/Settings/SettingOptionalExtruder.qml | 24 ++++++++++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/cura/Settings/ExtrudersModel.py b/cura/Settings/ExtrudersModel.py index 4ee5ab3c3b..f179dabd5a 100644 --- a/cura/Settings/ExtrudersModel.py +++ b/cura/Settings/ExtrudersModel.py @@ -210,6 +210,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel): item = { "id": "", "name": catalog.i18nc("@menuitem", "Not overridden"), + "enabled": True, "color": "#ffffff", "index": -1, "definition": "" diff --git a/resources/qml/Settings/SettingExtruder.qml b/resources/qml/Settings/SettingExtruder.qml index 2ddbb135c7..38b1c2cab0 100644 --- a/resources/qml/Settings/SettingExtruder.qml +++ b/resources/qml/Settings/SettingExtruder.qml @@ -215,7 +215,8 @@ SettingItem { text: model.name renderType: Text.NativeRendering - color: { + color: + { if (model.enabled) { UM.Theme.getColor("setting_control_text") } else { diff --git a/resources/qml/Settings/SettingOptionalExtruder.qml b/resources/qml/Settings/SettingOptionalExtruder.qml index f49b7035d7..a370ec6259 100644 --- a/resources/qml/Settings/SettingOptionalExtruder.qml +++ b/resources/qml/Settings/SettingOptionalExtruder.qml @@ -27,8 +27,19 @@ SettingItem onActivated: { - forceActiveFocus(); - propertyProvider.setPropertyValue("value", model.getItem(index).index); + if (model.getItem(index).enabled) + { + forceActiveFocus(); + propertyProvider.setPropertyValue("value", model.getItem(index).index); + } else + { + if (propertyProvider.properties.value == -1) + { + control.currentIndex = model.rowCount() - 1; // we know the last item is "Not overriden" + } else { + control.currentIndex = propertyProvider.properties.value; // revert to the old value + } + } } onActiveFocusChanged: @@ -192,7 +203,14 @@ SettingItem { text: model.name renderType: Text.NativeRendering - color: UM.Theme.getColor("setting_control_text") + color: + { + if (model.enabled) { + UM.Theme.getColor("setting_control_text") + } else { + UM.Theme.getColor("action_button_disabled_text"); + } + } font: UM.Theme.getFont("default") elide: Text.ElideRight verticalAlignment: Text.AlignVCenter From b9bf78d36ce67bbcd5163d479778b69b362b51ce Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 15:05:56 +0100 Subject: [PATCH 47/88] Remove group when "parent" is the only node in the group --- plugins/SupportEraser/SupportEraser.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index d87a887d1b..ee59fc5258 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -125,10 +125,24 @@ class SupportEraser(Tool): Selection.add(parent) def _removeEraserMesh(self, node: CuraSceneNode): - op = RemoveSceneNodeOperation(node) + group = node.getParent() + if group.callDecoration("isGroup"): + parent = group.getChildren()[0] + + op = GroupedOperation() + op.addOperation(RemoveSceneNodeOperation(node)) + if len(group.getChildren()) == 2: + op.addOperation(SetParentOperation(parent, group.getParent())) + op.push() Application.getInstance().getController().getScene().sceneChanged.emit(node) + # Select the picked node so the group does not get drawn as a wireframe (yet) + if Selection.isSelected(group): + Selection.remove(group) + if parent and not Selection.isSelected(parent): + Selection.add(parent) + def _updateEnabled(self): plugin_enabled = False From dcb68bb33ef2e65fc3b63c7ac1196fd45105822c Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 15 Mar 2018 15:13:35 +0100 Subject: [PATCH 48/88] CURA-4870 Move the checkCorrectGroupName to the machine manager, where it must belong to. --- .../NetworkedPrinterOutputDevice.py | 17 +++-------------- cura/Settings/MachineManager.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 0dca149e5a..1537d51919 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -4,6 +4,7 @@ from UM.Application import Application from UM.Logger import Logger from UM.Settings.ContainerRegistry import ContainerRegistry +from cura.CuraApplication import CuraApplication from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState @@ -255,20 +256,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._last_manager_create_time = time() self._manager.authenticationRequired.connect(self._onAuthenticationRequired) - self._checkCorrectGroupName() - - ## This method checks if the name of the group stored in the definition container is correct. - # After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group - # then all the container stacks are updated, both the current and the hidden ones. - def _checkCorrectGroupName(self): - global_container_stack = Application.getInstance().getGlobalContainerStack() - if global_container_stack and self.getId() == global_container_stack.getMetaDataEntry("um_network_key"): - # Check if the connect_group_name is correct. If not, update all the containers connected to the same printer - if global_container_stack.getMetaDataEntry("connect_group_name") != self.name: - metadata_filter = {"um_network_key": global_container_stack.getMetaDataEntry("um_network_key")} - hidden_containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) - for container in hidden_containers: - container.setMetaDataEntry("connect_group_name", self.name) + machine_manager = CuraApplication.getInstance().getMachineManager() + machine_manager.checkCorrectGroupName(self.getId(), self.name) def _registerOnFinishedCallback(self, reply: QNetworkReply, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: if onFinished is not None: diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 1cf3fae161..3a6015e90f 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -1201,6 +1201,18 @@ class MachineManager(QObject): if machine.getMetaDataEntry(key) == value: machine.setMetaDataEntry(key, new_value) + ## This method checks if the name of the group stored in the definition container is correct. + # After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group + # then all the container stacks are updated, both the current and the hidden ones. + def checkCorrectGroupName(self, device_id: str, group_name: str): + if self._global_container_stack and device_id == self.activeMachineNetworkKey: + # Check if the connect_group_name is correct. If not, update all the containers connected to the same printer + if self.activeMachineNetworkGroupName != group_name: + metadata_filter = {"um_network_key": self.activeMachineNetworkKey} + hidden_containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) + for container in hidden_containers: + container.setMetaDataEntry("connect_group_name", group_name) + @pyqtSlot("QVariant") def setGlobalVariant(self, container_node): self.blurSettings.emit() From 08f43f6b2e719cad4ac0c42efdfaa7d82af0dc37 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Mar 2018 15:27:13 +0100 Subject: [PATCH 49/88] Fix profile ordering in profile importing CURA-5054 --- cura/Settings/CuraContainerRegistry.py | 28 ++++++++++++++++---------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 0cf1c7399f..ab48eaddd2 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -173,12 +173,13 @@ class CuraContainerRegistry(ContainerRegistry): plugin_registry = PluginRegistry.getInstance() extension = file_name.split(".")[-1] - global_container_stack = Application.getInstance().getGlobalContainerStack() - if not global_container_stack: + global_stack = Application.getInstance().getGlobalContainerStack() + if not global_stack: return - machine_extruders = list(ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId())) - machine_extruders.sort(key = lambda k: k.getMetaDataEntry("position")) + machine_extruders = [] + for position in sorted(global_stack.extruders): + machine_extruders.append(global_stack.extruders[position]) for plugin_id, meta_data in self._getIOPlugins("profile_reader"): if meta_data["profile_reader"][0]["extension"] != extension: @@ -200,13 +201,18 @@ class CuraContainerRegistry(ContainerRegistry): # First check if this profile is suitable for this machine global_profile = None + extruder_profiles = [] if len(profile_or_list) == 1: global_profile = profile_or_list[0] else: for profile in profile_or_list: if not profile.getMetaDataEntry("position"): global_profile = profile - break + else: + extruder_profiles.append(profile) + extruder_profiles = sorted(extruder_profiles, key = lambda x: int(x.getMetaDataEntry("position"))) + profile_or_list = [global_profile] + extruder_profiles + if not global_profile: Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name) return { "status": "error", @@ -227,7 +233,7 @@ class CuraContainerRegistry(ContainerRegistry): # Get the expected machine definition. # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... profile_definition = getMachineDefinitionIDForQualitySearch(machine_definition) - expected_machine_definition = getMachineDefinitionIDForQualitySearch(global_container_stack.definition) + expected_machine_definition = getMachineDefinitionIDForQualitySearch(global_stack.definition) # And check if the profile_definition matches either one (showing error if not): if profile_definition != expected_machine_definition: @@ -251,8 +257,8 @@ class CuraContainerRegistry(ContainerRegistry): if len(profile_or_list) == 1: global_profile = profile_or_list[0] extruder_profiles = [] - for idx, extruder in enumerate(global_container_stack.extruders.values()): - profile_id = ContainerRegistry.getInstance().uniqueName(global_container_stack.getId() + "_extruder_" + str(idx + 1)) + for idx, extruder in enumerate(global_stack.extruders.values()): + profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1)) profile = InstanceContainer(profile_id) profile.setName(quality_name) profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) @@ -264,12 +270,12 @@ class CuraContainerRegistry(ContainerRegistry): if idx == 0: # move all per-extruder settings to the first extruder's quality_changes for qc_setting_key in global_profile.getAllKeys(): - settable_per_extruder = global_container_stack.getProperty(qc_setting_key, + settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder") if settable_per_extruder: setting_value = global_profile.getProperty(qc_setting_key, "value") - setting_definition = global_container_stack.getSettingDefinition(qc_setting_key) + setting_definition = global_stack.getSettingDefinition(qc_setting_key) new_instance = SettingInstance(setting_definition, profile) new_instance.setProperty("value", setting_value) new_instance.resetState() # Ensure that the state is not seen as a user state. @@ -286,7 +292,7 @@ class CuraContainerRegistry(ContainerRegistry): for profile_index, profile in enumerate(profile_or_list): if profile_index == 0: # This is assumed to be the global profile - profile_id = (global_container_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_") + profile_id = (global_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_") elif profile_index < len(machine_extruders) + 1: # This is assumed to be an extruder profile From 4b060f297f2a386ad8193f6795c1608ce5ec5830 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 15 Mar 2018 15:44:27 +0100 Subject: [PATCH 50/88] CURA-4400 grey out disabled extruder in context menu --- resources/qml/Menus/ContextMenu.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/Menus/ContextMenu.qml b/resources/qml/Menus/ContextMenu.qml index 83302f9463..e35aef5f20 100644 --- a/resources/qml/Menus/ContextMenu.qml +++ b/resources/qml/Menus/ContextMenu.qml @@ -31,7 +31,7 @@ Menu MenuItem { text: "%1: %2 - %3".arg(model.name).arg(model.material).arg(model.variant) visible: base.shouldShowExtruders - enabled: UM.Selection.hasSelection + enabled: UM.Selection.hasSelection && model.enabled checkable: true checked: Cura.ExtruderManager.selectedObjectExtruders.indexOf(model.id) != -1 onTriggered: CuraActions.setExtruderForSelection(model.id) From 8af82cc3f459e39abedd534e4295b0fd8ac1f04a Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 15 Mar 2018 15:54:44 +0100 Subject: [PATCH 51/88] CURA-4400 prevent disabling last enabled extruder --- cura/Settings/MachineManager.py | 9 ++++++++- resources/qml/Cura.qml | 1 + resources/qml/SidebarHeader.qml | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 3a6015e90f..50c3c53734 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -145,6 +145,7 @@ class MachineManager(QObject): activeStackValueChanged = pyqtSignal() # Emitted whenever a value inside the active stack is changed. activeStackValidationChanged = pyqtSignal() # Emitted whenever a validation inside active container is changed stacksValidationChanged = pyqtSignal() # Emitted whenever a validation is changed + numberExtrudersEnabledChanged = pyqtSignal() # Emitted when the number of extruders that are enabled changed blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly @@ -880,7 +881,13 @@ class MachineManager(QObject): for position, extruder in self._global_container_stack.extruders.items(): if extruder.isEnabled: extruder_count += 1 - definition_changes_container.setProperty("extruders_enabled_count", "value", extruder_count) + if self.numberExtrudersEnabled != extruder_count: + definition_changes_container.setProperty("extruders_enabled_count", "value", extruder_count) + self.numberExtrudersEnabledChanged.emit() + + @pyqtProperty(int, notify = numberExtrudersEnabledChanged) + def numberExtrudersEnabled(self): + return self._global_container_stack.definitionChanges.getProperty("extruders_enabled_count", "value") @pyqtProperty(str, notify = extruderChanged) def defaultExtruderPosition(self): diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index cff4399073..c4ebb790e8 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -217,6 +217,7 @@ UM.MainWindow text: catalog.i18nc("@action:inmenu", "Disable Extruder") onTriggered: Cura.MachineManager.setExtruderEnabled(model.index, false) visible: Cura.MachineManager.getExtruder(model.index).isEnabled + enabled: Cura.MachineManager.numberExtrudersEnabled > 1 } } diff --git a/resources/qml/SidebarHeader.qml b/resources/qml/SidebarHeader.qml index 5cd0446b36..92af6e9cc9 100644 --- a/resources/qml/SidebarHeader.qml +++ b/resources/qml/SidebarHeader.qml @@ -178,6 +178,7 @@ Column text: catalog.i18nc("@action:inmenu", "Disable Extruder") onTriggered: Cura.MachineManager.setExtruderEnabled(model.index, false) visible: extruder_enabled + enabled: Cura.MachineManager.numberExtrudersEnabled > 1 } } From b1c1b04f0d96c5b43795f44581691f8f3d634f7c Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Mar 2018 16:08:40 +0100 Subject: [PATCH 52/88] Restore change to SettingView removeUnusedValue --- resources/qml/Settings/SettingView.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/qml/Settings/SettingView.qml b/resources/qml/Settings/SettingView.qml index a6d7b3a71e..cf9697210b 100644 --- a/resources/qml/Settings/SettingView.qml +++ b/resources/qml/Settings/SettingView.qml @@ -440,6 +440,7 @@ Item key: model.key ? model.key : "" watchedProperties: [ "value", "enabled", "state", "validationState", "settable_per_extruder", "resolve" ] storeIndex: 0 + removeUnusedValue: model.resolve == undefined } Connections From f127660cfc84819d61a58727fd7e7fe96c1f4b8d Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 15 Mar 2018 16:12:32 +0100 Subject: [PATCH 53/88] Remove whitespace at the end of the lines Someone put tabs there. --- resources/definitions/ultimaker3.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/ultimaker3.def.json b/resources/definitions/ultimaker3.def.json index 57cfbe960f..ef41686752 100644 --- a/resources/definitions/ultimaker3.def.json +++ b/resources/definitions/ultimaker3.def.json @@ -110,9 +110,9 @@ "material_bed_temperature": { "maximum_value": "115" }, "material_bed_temperature_layer_0": { "maximum_value": "115" }, "material_standby_temperature": { "value": "100" }, - "meshfix_maximum_resolution": { "value": "0.04" }, + "meshfix_maximum_resolution": { "value": "0.04" }, "multiple_mesh_overlap": { "value": "0" }, - "optimize_wall_printing_order": { "value": "True" }, + "optimize_wall_printing_order": { "value": "True" }, "prime_tower_enable": { "default_value": true }, "raft_airgap": { "value": "0" }, "raft_base_thickness": { "value": "0.3" }, From 3ce51bb25554ef70c695302d36927fbbde7a4761 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 15 Mar 2018 16:14:20 +0100 Subject: [PATCH 54/88] CURA-4870 Correctly align the name with the connection icon when the printer is connected to USB --- resources/qml/MachineSelection.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/qml/MachineSelection.qml b/resources/qml/MachineSelection.qml index b959e20bb7..d075486eb2 100644 --- a/resources/qml/MachineSelection.qml +++ b/resources/qml/MachineSelection.qml @@ -71,8 +71,8 @@ ToolButton color: UM.Theme.getColor("sidebar_header_text_active") text: control.text; elide: Text.ElideRight; - anchors.left: isNetworkPrinter ? printerStatusIcon.right : parent.left; - anchors.leftMargin: isNetworkPrinter ? UM.Theme.getSize("sidebar_lining").width : UM.Theme.getSize("sidebar_margin").width + anchors.left: printerStatusIcon.visible ? printerStatusIcon.right : parent.left; + anchors.leftMargin: printerStatusIcon.visible ? UM.Theme.getSize("sidebar_lining").width : UM.Theme.getSize("sidebar_margin").width anchors.right: downArrow.left; anchors.rightMargin: control.rightMargin; anchors.verticalCenter: parent.verticalCenter; From 440a56b7fa6d4f09eb64f3969c85d4d8309b0c37 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Mar 2018 16:41:50 +0100 Subject: [PATCH 55/88] Fix settings export in gcode --- plugins/GCodeWriter/GCodeWriter.py | 63 ++++++++++++++++++------------ 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index 1b3b7264a1..4b78a2a72a 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -1,17 +1,17 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import re # For escaping characters in the settings. +import json +import copy + from UM.Mesh.MeshWriter import MeshWriter from UM.Logger import Logger from UM.Application import Application from UM.Settings.InstanceContainer import InstanceContainer -from UM.Util import parseBool -from cura.Settings.ExtruderManager import ExtruderManager +from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch -import re #For escaping characters in the settings. -import json -import copy ## Writes g-code to a file. # @@ -45,6 +45,8 @@ class GCodeWriter(MeshWriter): def __init__(self): super().__init__() + self._application = Application.getInstance() + ## Writes the g-code for the entire scene to a stream. # # Note that even though the function accepts a collection of nodes, the @@ -94,7 +96,6 @@ class GCodeWriter(MeshWriter): return flat_container - ## Serialises a container stack to prepare it for writing at the end of the # g-code. # @@ -104,15 +105,21 @@ class GCodeWriter(MeshWriter): # \param settings A container stack to serialise. # \return A serialised string of the settings. def _serialiseSettings(self, stack): + container_registry = self._application.getContainerRegistry() + quality_manager = self._application.getQualityManager() + prefix = ";SETTING_" + str(GCodeWriter.version) + " " # The prefix to put before each line. prefix_length = len(prefix) + quality_name = stack.qualityChanges.getName() + quality_type = stack.quality.getMetaDataEntry("quality_type") container_with_profile = stack.qualityChanges if container_with_profile.getId() == "empty_quality_changes": - Logger.log("e", "No valid quality profile found, not writing settings to g-code!") - return "" + # If the global quality changes is empty, create a new one + quality_name = container_registry.uniqueName(stack.quality.getName()) + container_with_profile = quality_manager._createQualityChanges(quality_type, quality_name, stack, None) - flat_global_container = self._createFlattenedContainerInstance(stack.getTop(), container_with_profile) + flat_global_container = self._createFlattenedContainerInstance(stack.userChanges, container_with_profile) # If the quality changes is not set, we need to set type manually if flat_global_container.getMetaDataEntry("type", None) is None: flat_global_container.addMetaDataEntry("type", "quality_changes") @@ -121,41 +128,47 @@ class GCodeWriter(MeshWriter): if flat_global_container.getMetaDataEntry("quality_type", None) is None: flat_global_container.addMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal")) - # Change the default defintion - default_machine_definition = "fdmprinter" - if parseBool(stack.getMetaDataEntry("has_machine_quality", "False")): - default_machine_definition = stack.getMetaDataEntry("quality_definition") - if not default_machine_definition: - default_machine_definition = stack.definition.getId() - flat_global_container.setMetaDataEntry("definition", default_machine_definition) + # Get the machine definition ID for quality profiles + machine_definition_id_for_quality = getMachineDefinitionIDForQualitySearch(stack.definition) + flat_global_container.setMetaDataEntry("definition", machine_definition_id_for_quality) serialized = flat_global_container.serialize() data = {"global_quality": serialized} - for extruder in sorted(stack.extruders.values(), key = lambda k: k.getMetaDataEntry("position")): + all_setting_keys = set(flat_global_container.getAllKeys()) + for extruder in sorted(stack.extruders.values(), key = lambda k: int(k.getMetaDataEntry("position"))): extruder_quality = extruder.qualityChanges if extruder_quality.getId() == "empty_quality_changes": - Logger.log("w", "No extruder quality profile found, not writing quality for extruder %s to file!", extruder.getId()) - continue - flat_extruder_quality = self._createFlattenedContainerInstance(extruder.getTop(), extruder_quality) + # Same story, if quality changes is empty, create a new one + quality_name = container_registry.uniqueName(stack.quality.getName()) + extruder_quality = quality_manager._createQualityChanges(quality_type, quality_name, stack, None) + + flat_extruder_quality = self._createFlattenedContainerInstance(extruder.userChanges, extruder_quality) # If the quality changes is not set, we need to set type manually if flat_extruder_quality.getMetaDataEntry("type", None) is None: flat_extruder_quality.addMetaDataEntry("type", "quality_changes") # Ensure that extruder is set. (Can happen if we have empty quality changes). - if flat_extruder_quality.getMetaDataEntry("extruder", None) is None: - flat_extruder_quality.addMetaDataEntry("extruder", extruder.getBottom().getId()) + if flat_extruder_quality.getMetaDataEntry("position", None) is None: + flat_extruder_quality.addMetaDataEntry("position", extruder.getMetaDataEntry("position")) # Ensure that quality_type is set. (Can happen if we have empty quality changes). if flat_extruder_quality.getMetaDataEntry("quality_type", None) is None: flat_extruder_quality.addMetaDataEntry("quality_type", extruder.quality.getMetaDataEntry("quality_type", "normal")) - # Change the default defintion - flat_extruder_quality.setMetaDataEntry("definition", default_machine_definition) + # Change the default definition + flat_extruder_quality.setMetaDataEntry("definition", machine_definition_id_for_quality) extruder_serialized = flat_extruder_quality.serialize() data.setdefault("extruder_quality", []).append(extruder_serialized) + all_setting_keys.update(set(flat_extruder_quality.getAllKeys())) + + # Check if there is any profiles + if not all_setting_keys: + Logger.log("i", "No custom settings found, not writing settings to g-code.") + return "" + json_string = json.dumps(data) # Escape characters that have a special meaning in g-code comments. @@ -169,5 +182,5 @@ class GCodeWriter(MeshWriter): # Lines have 80 characters, so the payload of each line is 80 - prefix. for pos in range(0, len(escaped_string), 80 - prefix_length): - result += prefix + escaped_string[pos : pos + 80 - prefix_length] + "\n" + result += prefix + escaped_string[pos: pos + 80 - prefix_length] + "\n" return result From d0e6e59845266fcb8c35cb1f9a11feec1d45139c Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 15 Mar 2018 16:46:25 +0100 Subject: [PATCH 56/88] Set the frame visible for the materials and profiles page in preferences. --- resources/qml/Preferences/MaterialsPage.qml | 1 + resources/qml/Preferences/ProfilesPage.qml | 1 + 2 files changed, 2 insertions(+) diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index 7f06ffecde..042bd09828 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -364,6 +364,7 @@ Item } width: true ? (parent.width * 0.4) | 0 : parent.width + frameVisible: true ListView { diff --git a/resources/qml/Preferences/ProfilesPage.qml b/resources/qml/Preferences/ProfilesPage.qml index ff35e27eeb..1726087e97 100644 --- a/resources/qml/Preferences/ProfilesPage.qml +++ b/resources/qml/Preferences/ProfilesPage.qml @@ -369,6 +369,7 @@ Item } width: true ? (parent.width * 0.4) | 0 : parent.width + frameVisible: true ListView { From 9de114c73ab5976466cc222851666525c054bd6d Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Mar 2018 16:47:51 +0100 Subject: [PATCH 57/88] Simplify discardOrKeepProfileChangesClosed() --- cura/CuraApplication.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 1b5de89c2b..6056745c75 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -451,27 +451,18 @@ class CuraApplication(QtApplication): @pyqtSlot(str) def discardOrKeepProfileChangesClosed(self, option): + global_stack = self.getGlobalContainerStack() if option == "discard": - global_stack = self.getGlobalContainerStack() - for extruder in self._extruder_manager.getMachineExtruders(global_stack.getId()): - extruder.getTop().clear() - global_stack.getTop().clear() + for extruder in global_stack.extruders.values(): + extruder.userChanges.clear() + global_stack.userChanges.clear() # if the user decided to keep settings then the user settings should be re-calculated and validated for errors # before slicing. To ensure that slicer uses right settings values elif option == "keep": - global_stack = self.getGlobalContainerStack() - for extruder in self._extruder_manager.getMachineExtruders(global_stack.getId()): - user_extruder_container = extruder.getTop() - if user_extruder_container: - user_extruder_container.update() - - user_global_container = global_stack.getTop() - if user_global_container: - user_global_container.update() - - # notify listeners that quality has changed (after user selected discard or keep) - self.getMachineManager().activeQualityChanged.emit() + for extruder in global_stack.extruders.values(): + extruder.userChanges.update() + global_stack.userChanges.update() @pyqtSlot(int) def messageBoxClosed(self, button): From 40ba06f011eccbe0f12d1b42beccc4dfe83f7cb2 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 15 Mar 2018 16:45:04 +0100 Subject: [PATCH 58/88] Fix output type of BQ Hephestos That application/x-code may actually be the XCode application or something? Whatever, it should be x-gcode... --- resources/definitions/bq_hephestos_xl.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/bq_hephestos_xl.def.json b/resources/definitions/bq_hephestos_xl.def.json index 75b756c71e..08be4b8d34 100644 --- a/resources/definitions/bq_hephestos_xl.def.json +++ b/resources/definitions/bq_hephestos_xl.def.json @@ -6,7 +6,7 @@ "visible": true, "manufacturer": "BQ", "author": "BQ", - "file_formats": "text/x-code", + "file_formats": "text/x-gcode", "platform": "bq_hephestos_platform.stl", "platform_offset": [ 0, -82, 0] }, From b0f7a5b3588966ae088df610626b1578c17e9540 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 15 Mar 2018 16:52:16 +0100 Subject: [PATCH 59/88] Fix translation of there being no file formats available This entry said that it had a context, but it had none. As a result, the whole string was seen as a context. --- .../RemovableDriveOutputDevice/RemovableDriveOutputDevice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py index ff930e2c31..1bfdbd4117 100644 --- a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py +++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py @@ -60,7 +60,7 @@ class RemovableDriveOutputDevice(OutputDevice): if len(file_formats) == 0: Logger.log("e", "There are no file formats available to write with!") - raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("There are no file formats available to write with!")) + raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status", "There are no file formats available to write with!")) # Just take the first file format available. if file_handler is not None: From b8ab623c803f343e09e1f1a69412e54c2ca13ad5 Mon Sep 17 00:00:00 2001 From: Aleksei S Date: Thu, 15 Mar 2018 16:54:39 +0100 Subject: [PATCH 60/88] Fix: Infill slider did not work at first Cura start CURA-5071 --- resources/qml/SidebarSimple.qml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/resources/qml/SidebarSimple.qml b/resources/qml/SidebarSimple.qml index 41ecb529eb..c02337d1f5 100644 --- a/resources/qml/SidebarSimple.qml +++ b/resources/qml/SidebarSimple.qml @@ -594,7 +594,9 @@ Item // Update value only if the Recomended mode is Active, // Otherwise if I change the value in the Custom mode the Recomended view will try to repeat // same operation - if (UM.Preferences.getValue("cura/active_mode") == 0) { + var active_mode = UM.Preferences.getValue("cura/active_mode") + + if (active_mode == 0 || active_mode == "simple") { Cura.MachineManager.setSettingForAllExtruders("infill_sparse_density", "value", roundedSliderValue) } } From 3bb0a481f1a4e2ba26d494fc803d5394877d3ac8 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Mar 2018 17:00:45 +0100 Subject: [PATCH 61/88] Simplify default quality reset --- cura/Settings/MachineManager.py | 9 +++++++++ resources/qml/SidebarSimple.qml | 15 ++------------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 50c3c53734..b614e1e1e8 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -1269,6 +1269,15 @@ class MachineManager(QObject): if not no_dialog and self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1: self._application.discardOrKeepProfileChanges() + @pyqtSlot() + def clearQualityChangesGroup(self): + with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): + self._setQualityGroup(self._current_quality_group) + + # See if we need to show the Discard or Keep changes screen + if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1: + self._application.discardOrKeepProfileChanges() + @pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged) def activeQualityChangesGroup(self): return self._current_quality_changes_group diff --git a/resources/qml/SidebarSimple.qml b/resources/qml/SidebarSimple.qml index c02337d1f5..a8af3fc3dd 100644 --- a/resources/qml/SidebarSimple.qml +++ b/resources/qml/SidebarSimple.qml @@ -19,7 +19,7 @@ Item property Action configureSettings; property variant minimumPrintTime: PrintInformation.minimumPrintTime; property variant maximumPrintTime: PrintInformation.maximumPrintTime; - property bool settingsEnabled: Cura.ExtruderManager.activeExtruderStackId || extrudersEnabledCount.properties.value == 1 + property bool settingsEnabled: extrudersEnabledCount.properties.value == 1 Component.onCompleted: PrintInformation.enabled = true Component.onDestruction: PrintInformation.enabled = false @@ -474,18 +474,7 @@ Item onClicked: { // if the current profile is user-created, switch to a built-in quality - if (Cura.SimpleModeSettingsManager.isProfileUserCreated) - { - if (Cura.QualityProfilesDropDownMenuModel.rowCount() > 0) - { - var item = Cura.QualityProfilesDropDownMenuModel.getItem(0); - Cura.MachineManager.activeQualityGroup = item.quality_group; - } - } - if (Cura.SimpleModeSettingsManager.isProfileCustomized) - { - discardOrKeepProfileChangesDialog.show() - } + Cura.MachineManager.clearQualityChangesGroup() } onEntered: { From f8709b6d1af90483c7216cfe28635a4e633f1079 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Thu, 15 Mar 2018 17:02:36 +0100 Subject: [PATCH 62/88] CURA-4400 improved enabled / disabled extruder visually, improved colors of dark theme --- resources/qml/SidebarHeader.qml | 44 ++++++++++++++++++++------- resources/themes/cura-dark/theme.json | 6 ++-- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/resources/qml/SidebarHeader.qml b/resources/qml/SidebarHeader.qml index 92af6e9cc9..7baf13ca97 100644 --- a/resources/qml/SidebarHeader.qml +++ b/resources/qml/SidebarHeader.qml @@ -186,22 +186,34 @@ Column { background: Item { - Rectangle + function buttonBackgroundColor(index) { - anchors.fill: parent - 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") - color: (control.checked || control.pressed) ? UM.Theme.getColor("action_button_active") : - control.hovered ? UM.Theme.getColor("action_button_hovered") : - UM.Theme.getColor("action_button") - Behavior on color { ColorAnimation { duration: 50; } } + var extruder = Cura.MachineManager.getExtruder(index) + if (extruder.isEnabled) { + return (control.checked || control.pressed) ? UM.Theme.getColor("action_button_active") : + control.hovered ? UM.Theme.getColor("action_button_hovered") : + UM.Theme.getColor("action_button") + } else { + return UM.Theme.getColor("action_button_disabled") + } + } + + function buttonBorderColor(index) + { + var extruder = Cura.MachineManager.getExtruder(index) + if (extruder.isEnabled) { + return (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") + } else { + return UM.Theme.getColor("action_button_disabled_border") + } } function buttonColor(index) { var extruder = Cura.MachineManager.getExtruder(index); - if (extruder.isEnabled) { + if (extruder.isEnabled) + { return ( control.checked || control.pressed) ? UM.Theme.getColor("action_button_active_text") : control.hovered ? UM.Theme.getColor("action_button_hovered_text") : @@ -211,10 +223,20 @@ Column } } + Rectangle + { + anchors.fill: parent + border.width: control.checked ? UM.Theme.getSize("default_lining").width * 2 : UM.Theme.getSize("default_lining").width + border.color: buttonBorderColor(index) + color: buttonBackgroundColor(index) + Behavior on color { ColorAnimation { duration: 50; } } + } + Item { id: extruderButtonFace anchors.centerIn: parent + width: { var extruderTextWidth = extruderStaticText.visible ? extruderStaticText.width : 0; var iconWidth = extruderIconItem.width; diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index 80a5eec09c..5fbe36fcdb 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -84,16 +84,16 @@ "tab_background": [39, 44, 48, 255], "action_button": [39, 44, 48, 255], - "action_button_text": [255, 255, 255, 101], + "action_button_text": [255, 255, 255, 200], "action_button_border": [255, 255, 255, 30], "action_button_hovered": [39, 44, 48, 255], "action_button_hovered_text": [255, 255, 255, 255], "action_button_hovered_border": [255, 255, 255, 30], "action_button_active": [39, 44, 48, 30], "action_button_active_text": [255, 255, 255, 255], - "action_button_active_border": [255, 255, 255, 30], + "action_button_active_border": [255, 255, 255, 100], "action_button_disabled": [39, 44, 48, 255], - "action_button_disabled_text": [255, 255, 255, 101], + "action_button_disabled_text": [255, 255, 255, 80], "action_button_disabled_border": [255, 255, 255, 30], "scrollbar_background": [39, 44, 48, 0], From f14ddb8711ac1cdcb91aa33cc82e7ca890ed2c14 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Mar 2018 17:08:44 +0100 Subject: [PATCH 63/88] Fix reset to default quality --- cura/Settings/MachineManager.py | 8 +++----- cura/Settings/SimpleModeSettingsManager.py | 3 ++- resources/qml/SidebarSimple.qml | 3 +-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index b614e1e1e8..9619329e59 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -1270,13 +1270,11 @@ class MachineManager(QObject): self._application.discardOrKeepProfileChanges() @pyqtSlot() - def clearQualityChangesGroup(self): + def resetToUseDefaultQuality(self): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): self._setQualityGroup(self._current_quality_group) - - # See if we need to show the Discard or Keep changes screen - if self.hasUserSettings and Preferences.getInstance().getValue("cura/active_mode") == 1: - self._application.discardOrKeepProfileChanges() + for stack in [self._global_container_stack] + list(self._global_container_stack.extruders.values()): + stack.userChanges.clear() @pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged) def activeQualityChangesGroup(self): diff --git a/cura/Settings/SimpleModeSettingsManager.py b/cura/Settings/SimpleModeSettingsManager.py index 867a21702c..a337d8b04e 100644 --- a/cura/Settings/SimpleModeSettingsManager.py +++ b/cura/Settings/SimpleModeSettingsManager.py @@ -16,7 +16,8 @@ class SimpleModeSettingsManager(QObject): self._is_profile_user_created = False # True when profile was custom created by user self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized) - self._machine_manager.activeQualityChanged.connect(self._updateIsProfileUserCreated) + self._machine_manager.activeQualityGroupChanged.connect(self._updateIsProfileUserCreated) + self._machine_manager.activeQualityChangesGroupChanged.connect(self._updateIsProfileUserCreated) # update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts self._updateIsProfileCustomized() diff --git a/resources/qml/SidebarSimple.qml b/resources/qml/SidebarSimple.qml index a8af3fc3dd..aa6f3ce1de 100644 --- a/resources/qml/SidebarSimple.qml +++ b/resources/qml/SidebarSimple.qml @@ -111,7 +111,6 @@ Item // Set selected value if (Cura.MachineManager.activeQualityType == qualityItem.quality_type) { - // set to -1 when switching to user created profile so all ticks are clickable if (Cura.SimpleModeSettingsManager.isProfileUserCreated) { qualityModel.qualitySliderActiveIndex = -1 @@ -474,7 +473,7 @@ Item onClicked: { // if the current profile is user-created, switch to a built-in quality - Cura.MachineManager.clearQualityChangesGroup() + Cura.MachineManager.resetToUseDefaultQuality() } onEntered: { From 99d653cea5f04b346341fd4dffe4c7b7d87b3a13 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Thu, 15 Mar 2018 20:01:47 +0100 Subject: [PATCH 64/88] CURA-4870 Don't allow to connect more than one instance to the same group. --- cura/Settings/MachineManager.py | 8 ++++++ .../UM3NetworkPrinting/DiscoverUM3Action.py | 4 +++ .../UM3NetworkPrinting/DiscoverUM3Action.qml | 28 ++++++++++++++++--- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 9619329e59..b69d91c0d4 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -1220,6 +1220,14 @@ class MachineManager(QObject): for container in hidden_containers: container.setMetaDataEntry("connect_group_name", group_name) + ## This method checks if there is an instance connected to the given network_key + def existNetworkInstances(self, network_key: str) -> bool: + metadata_filter = {"um_network_key": network_key} + containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) + if containers: + return True + return False + @pyqtSlot("QVariant") def setGlobalVariant(self, container_node): self.blurSettings.emit() diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py index 76e8721fdd..0b8d6e9f53 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py @@ -147,6 +147,10 @@ class DiscoverUM3Action(MachineAction): return "" + @pyqtSlot(str, result = bool) + def existsKey(self, key) -> bool: + return Application.getInstance().getMachineManager().existNetworkInstances(network_key = key) + @pyqtSlot() def loadConfigurationFromPrinter(self): machine_manager = Application.getInstance().getMachineManager() diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml index 079e5dcdd3..e7df22b546 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml @@ -5,6 +5,7 @@ import QtQuick 2.2 import QtQuick.Controls 1.1 import QtQuick.Layouts 1.1 import QtQuick.Window 2.1 +import QtQuick.Dialogs 1.2 Cura.MachineAction { @@ -33,15 +34,34 @@ Cura.MachineAction { var printerKey = base.selectedDevice.key var printerName = base.selectedDevice.name // TODO To change when the groups have a name - if(manager.getStoredKey() != printerKey) + if (manager.getStoredKey() != printerKey) { - manager.setKey(printerKey) - manager.setGroupName(printerName) // TODO To change when the groups have a name - completed() + // Check if there is another instance with the same key + if (!manager.existsKey(printerKey)) + { + manager.setKey(printerKey) + manager.setGroupName(printerName) // TODO To change when the groups have a name + completed() + } + else + { + existingConnectionDialog.open() + } } } } + MessageDialog + { + id: existingConnectionDialog + title: catalog.i18nc("@window:title", "Existing Connection") + icon: StandardIcon.Information + text: catalog.i18nc("@message:text", "There is an instance already connected to this group") + detailedText: catalog.i18nc("@message:description", "You can't connect two instances to the same group. Please use the other instance or connect to another group.") + standardButtons: StandardButton.Ok + modality: Qt.ApplicationModal + } + Column { anchors.fill: parent; From 2a811c62d8c2b3dcb07ab84378a33047a232c092 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Thu, 15 Mar 2018 23:58:22 +0100 Subject: [PATCH 65/88] Ignore the first press after the selection has been cleared if the selection is cleared with this tool active, there is no way to switch to another tool than to re-select an object (by clicking it) because the tool buttons in the toolbar will have been disabled. The mouse-event happens after the selection is changed, so we need to keep track of what the selection was previously by monitoring the selection state. --- plugins/SupportEraser/SupportEraser.py | 47 +++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index ee59fc5258..2afb3dfaab 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -4,7 +4,7 @@ import os import os.path -from PyQt5.QtCore import Qt, QUrl +from PyQt5.QtCore import Qt, QTimer from UM.Math.Vector import Vector from UM.Tool import Tool @@ -39,10 +39,28 @@ class SupportEraser(Tool): self._selection_pass = None Application.getInstance().globalContainerStackChanged.connect(self._updateEnabled) + # Note: if the selection is cleared with this tool active, there is no way to switch to + # another tool than to reselect an object (by clicking it) because the tool buttons in the + # toolbar will have been disabled. That is why we need to ignore the first press event + # after the selection has been cleared. + Selection.selectionChanged.connect(self._onSelectionChanged) + self._had_selection = False + self._skip_press = False + + self._had_selection_timer = QTimer() + self._had_selection_timer.setInterval(0) + self._had_selection_timer.setSingleShot(True) + self._had_selection_timer.timeout.connect(self._selectionChangeDelay) + def event(self, event): super().event(event) if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): + if self._skip_press: + # The selection was previously cleared, do not add/remove an anti-support mesh but + # use this click for selection and reactivating this tool only. + self._skip_press = False + return if self._selection_pass is None: # The selection renderpass is used to identify objects in the current view @@ -119,10 +137,10 @@ class SupportEraser(Tool): Application.getInstance().getController().getScene().sceneChanged.emit(node) # Select the picked node so the group does not get drawn as a wireframe (yet) - if Selection.isSelected(group): - Selection.remove(group) if not Selection.isSelected(parent): Selection.add(parent) + if Selection.isSelected(group): + Selection.remove(group) def _removeEraserMesh(self, node: CuraSceneNode): group = node.getParent() @@ -138,10 +156,10 @@ class SupportEraser(Tool): Application.getInstance().getController().getScene().sceneChanged.emit(node) # Select the picked node so the group does not get drawn as a wireframe (yet) - if Selection.isSelected(group): - Selection.remove(group) if parent and not Selection.isSelected(parent): Selection.add(parent) + if Selection.isSelected(group): + Selection.remove(group) def _updateEnabled(self): plugin_enabled = False @@ -151,3 +169,22 @@ class SupportEraser(Tool): plugin_enabled = global_container_stack.getProperty("anti_overhang_mesh", "enabled") Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled) + + + + def _onSelectionChanged(self): + # When selection is passed from one object to another object, first the selection is cleared + # and then it is set to the new object. We are only interested in the change from no selection + # to a selection or vice-versa, not in a change from one object to another. A timer is used to + # "merge" a possible clear/select action in a single frame + if Selection.hasSelection() != self._had_selection: + self._had_selection_timer.start() + + def _selectionChangeDelay(self): + has_selection = Selection.hasSelection() + if not has_selection and self._had_selection: + self._skip_press = True + else: + self._skip_press = False + + self._had_selection = has_selection From 579df7537be4a48be1f7e07bc119e5211e92b58f Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 16 Mar 2018 10:03:36 +0100 Subject: [PATCH 66/88] Fix upgrade from 26 to 27 Missing definition changes container. --- .../VersionUpgrade26to27/VersionUpgrade26to27.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/VersionUpgrade/VersionUpgrade26to27/VersionUpgrade26to27.py b/plugins/VersionUpgrade/VersionUpgrade26to27/VersionUpgrade26to27.py index a2d78d8d9f..2037a0211d 100644 --- a/plugins/VersionUpgrade/VersionUpgrade26to27/VersionUpgrade26to27.py +++ b/plugins/VersionUpgrade/VersionUpgrade26to27/VersionUpgrade26to27.py @@ -153,6 +153,10 @@ class VersionUpgrade26to27(VersionUpgrade): if new_id is not None: parser.set("containers", key, new_id) + if "6" not in parser["containers"]: + parser["containers"]["6"] = parser["containers"]["5"] + parser["containers"]["5"] = "empty" + for each_section in ("general", "metadata"): if not parser.has_section(each_section): parser.add_section(each_section) From 518423f25b5a7d84c684ba52cce2451152689124 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 16 Mar 2018 10:14:08 +0100 Subject: [PATCH 67/88] Fix broken settingsEnabled --- resources/qml/SidebarSimple.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/SidebarSimple.qml b/resources/qml/SidebarSimple.qml index aa6f3ce1de..1feabfb40f 100644 --- a/resources/qml/SidebarSimple.qml +++ b/resources/qml/SidebarSimple.qml @@ -19,7 +19,7 @@ Item property Action configureSettings; property variant minimumPrintTime: PrintInformation.minimumPrintTime; property variant maximumPrintTime: PrintInformation.maximumPrintTime; - property bool settingsEnabled: extrudersEnabledCount.properties.value == 1 + property bool settingsEnabled: Cura.ExtruderManager.activeExtruderStackId || extrudersEnabledCount.properties.value == 1 Component.onCompleted: PrintInformation.enabled = true Component.onDestruction: PrintInformation.enabled = false From d78e2d56f7b49527a654083490cfabccd3fdecf3 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 16 Mar 2018 10:33:20 +0100 Subject: [PATCH 68/88] Fix quality and variant processing in project loading From version 22 to 24, there is an upgrade for "quality" that changes it to "quality_changes", but for "quality", we don't have any upgrades. --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index f5daa77bb0..214623c92d 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -280,6 +280,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Check if any quality_changes instance container is in conflict. instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] quality_name = "" + custom_quality_name = "" num_settings_overriden_by_quality_changes = 0 # How many settings are changed by the quality changes num_user_settings = 0 quality_changes_conflict = False @@ -292,7 +293,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader): container_id = self._stripFileToId(instance_container_file_name) serialized = archive.open(instance_container_file_name).read().decode("utf-8") - serialized = InstanceContainer._updateSerialized(serialized, instance_container_file_name) + + # Qualities and variants don't have upgrades, so don't upgrade them + parser = ConfigParser(interpolation = None) + parser.read_string(serialized) + container_type = parser["metadata"]["type"] + if container_type not in ("quality", "variant"): + serialized = InstanceContainer._updateSerialized(serialized, instance_container_file_name) + parser = ConfigParser(interpolation = None) parser.read_string(serialized) container_info = ContainerInfo(instance_container_file_name, serialized, parser) @@ -309,7 +317,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): position = parser["metadata"]["position"] self._machine_info.quality_changes_info.extruder_info_dict[position] = container_info - quality_name = parser["general"]["name"] + custom_quality_name = parser["general"]["name"] values = parser["values"] if parser.has_section("values") else dict() num_settings_overriden_by_quality_changes += len(values) # Check if quality changes already exists. @@ -473,6 +481,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): extruders = num_extruders * [""] + quality_name = custom_quality_name if custom_quality_name else quality_name + self._machine_info.container_id = global_stack_id self._machine_info.name = machine_name self._machine_info.definition_id = machine_definition_id From a11595657518bf9436725c06b083bc3e854c3430 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 16 Mar 2018 10:40:16 +0100 Subject: [PATCH 69/88] Do not show a material itself in its linked material list --- cura/Settings/ContainerManager.py | 7 +++++-- resources/qml/Preferences/MaterialView.qml | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 7161169b22..760a288b7b 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -348,15 +348,18 @@ class ContainerManager(QObject): # # \param material_id \type{str} the id of the material for which to get the linked materials. # \return \type{list} a list of names of materials with the same GUID - @pyqtSlot("QVariant", result = "QStringList") - def getLinkedMaterials(self, material_node): + @pyqtSlot("QVariant", bool, result = "QStringList") + def getLinkedMaterials(self, material_node, exclude_self = False): guid = material_node.metadata["GUID"] + self_root_material_id = material_node.metadata["base_file"] material_group_list = self._material_manager.getMaterialGroupListByGUID(guid) linked_material_names = [] if material_group_list: for material_group in material_group_list: + if exclude_self and material_group.name == self_root_material_id: + continue linked_material_names.append(material_group.root_material_node.metadata["name"]) return linked_material_names diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml index d2f653e650..50dc6b65a4 100644 --- a/resources/qml/Preferences/MaterialView.qml +++ b/resources/qml/Preferences/MaterialView.qml @@ -36,8 +36,8 @@ TabView if (!base.containerId || !base.editingEnabled) { return "" } - var linkedMaterials = Cura.ContainerManager.getLinkedMaterials(base.currentMaterialNode); - if (linkedMaterials.length <= 1) { + var linkedMaterials = Cura.ContainerManager.getLinkedMaterials(base.currentMaterialNode, true); + if (linkedMaterials.length == 0) { return "" } return linkedMaterials.join(", "); From 06d028652d3b58aebf93b0b2653cde8af203a744 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 16 Mar 2018 10:58:31 +0100 Subject: [PATCH 70/88] CURA-4870 Change code style --- cura/Settings/MachineManager.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index b69d91c0d4..77cce9cddb 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -1224,9 +1224,7 @@ class MachineManager(QObject): def existNetworkInstances(self, network_key: str) -> bool: metadata_filter = {"um_network_key": network_key} containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) - if containers: - return True - return False + return bool(containers) @pyqtSlot("QVariant") def setGlobalVariant(self, container_node): From 6058f632df6226a27f8a21b21ed7ac252ddab1b9 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 16 Mar 2018 11:05:13 +0100 Subject: [PATCH 71/88] Fix Enter key handling on material management page --- resources/qml/Preferences/MaterialView.qml | 11 +++++++++++ resources/qml/Preferences/ReadOnlySpinBox.qml | 2 ++ resources/qml/Preferences/ReadOnlyTextField.qml | 2 ++ 3 files changed, 15 insertions(+) diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml index 50dc6b65a4..ffc4eaba3a 100644 --- a/resources/qml/Preferences/MaterialView.qml +++ b/resources/qml/Preferences/MaterialView.qml @@ -99,6 +99,7 @@ TabView property var new_diameter_value: null; property var old_diameter_value: null; property var old_approximate_diameter_value: null; + property bool keyPressed: false onYes: { @@ -112,6 +113,16 @@ TabView properties.diameter = old_diameter_value; diameterSpinBox.value = properties.diameter; } + + onVisibilityChanged: + { + if (!visible && !keyPressed) + { + // If the user closes this dialog without clicking on any button, it's the same as clicking "No". + no(); + } + keyPressed = false; + } } Label { width: scrollView.columnWidth; height: parent.rowHeight; verticalAlignment: Qt.AlignVCenter; text: catalog.i18nc("@label", "Display Name") } diff --git a/resources/qml/Preferences/ReadOnlySpinBox.qml b/resources/qml/Preferences/ReadOnlySpinBox.qml index 5d0666d306..1bbef82b1e 100644 --- a/resources/qml/Preferences/ReadOnlySpinBox.qml +++ b/resources/qml/Preferences/ReadOnlySpinBox.qml @@ -34,6 +34,8 @@ Item anchors.fill: parent onEditingFinished: base.editingFinished() + Keys.onEnterPressed: base.editingFinished() + Keys.onReturnPressed: base.editingFinished() } Label diff --git a/resources/qml/Preferences/ReadOnlyTextField.qml b/resources/qml/Preferences/ReadOnlyTextField.qml index 9407475a9b..38d07d7d6a 100644 --- a/resources/qml/Preferences/ReadOnlyTextField.qml +++ b/resources/qml/Preferences/ReadOnlyTextField.qml @@ -29,6 +29,8 @@ Item anchors.fill: parent onEditingFinished: base.editingFinished() + Keys.onEnterPressed: base.editingFinished() + Keys.onReturnPressed: base.editingFinished() } Label From a03a57fd60c71a31ed1dabbacfaf6a1a54462837 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 16 Mar 2018 11:10:35 +0100 Subject: [PATCH 72/88] Change the URL for redirect when checking material compatibility. --- resources/qml/SidebarHeader.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/qml/SidebarHeader.qml b/resources/qml/SidebarHeader.qml index 7baf13ca97..1ba04c387e 100644 --- a/resources/qml/SidebarHeader.qml +++ b/resources/qml/SidebarHeader.qml @@ -524,6 +524,8 @@ Column source: UM.Theme.getIcon("warning") width: UM.Theme.getSize("section_icon").width height: UM.Theme.getSize("section_icon").height + sourceSize.width: width + sourceSize.height: height color: UM.Theme.getColor("material_compatibility_warning") visible: !Cura.MachineManager.isCurrentSetupSupported } @@ -545,9 +547,7 @@ Column hoverEnabled: true onClicked: { // open the material URL with web browser - var version = UM.Application.version; - var machineName = Cura.MachineManager.activeMachine.definition.id; - var url = "https://ultimaker.com/materialcompatibility/" + version + "/" + machineName + "?utm_source=cura&utm_medium=software&utm_campaign=resources"; + var url = "https://ultimaker.com/incoming-links/cura/material-compatibilty" Qt.openUrlExternally(url); } onEntered: { From 32cee75e47560d5a5f87524e653a7c8b09c0d50c Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 15 Mar 2018 17:14:06 +0100 Subject: [PATCH 73/88] Respect order of preference of output formats Contributes to issue CURA-5097. --- .../RemovableDriveOutputDevice.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py index 1bfdbd4117..e719ba3286 100644 --- a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py +++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py @@ -1,4 +1,4 @@ -# Copyright (c) 2016 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os.path @@ -39,7 +39,7 @@ class RemovableDriveOutputDevice(OutputDevice): # MIME types available to the currently active machine? # def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): - filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do) + filter_by_machine = True # This plugin is intended to be used by machine (regardless of what it was told to do) if self._writing: raise OutputDeviceError.DeviceBusyError() @@ -56,7 +56,8 @@ class RemovableDriveOutputDevice(OutputDevice): machine_file_formats = [file_type.strip() for file_type in container.getMetaDataEntry("file_formats").split(";")] # Take the intersection between file_formats and machine_file_formats. - file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats)) + format_by_mimetype = {format["mime_type"]: format for format in file_formats} + file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats. if len(file_formats) == 0: Logger.log("e", "There are no file formats available to write with!") From 747efda52b6b69be4871956b7248e1870d55ad6a Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 16 Mar 2018 11:51:52 +0100 Subject: [PATCH 74/88] CURA-4870 Hide the configuration selector if the connection is lost. --- .../qml/Menus/ConfigurationMenu/SyncButton.qml | 17 ++++++++--------- resources/qml/Sidebar.qml | 2 +- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/resources/qml/Menus/ConfigurationMenu/SyncButton.qml b/resources/qml/Menus/ConfigurationMenu/SyncButton.qml index c292a792db..8fe9dacf9a 100644 --- a/resources/qml/Menus/ConfigurationMenu/SyncButton.qml +++ b/resources/qml/Menus/ConfigurationMenu/SyncButton.qml @@ -68,7 +68,8 @@ Button color: UM.Theme.getColor("text_emphasis") source: UM.Theme.getIcon("arrow_bottom") } - UM.RecolorImage { + UM.RecolorImage + { id: sidebarComboBoxLabel anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("default_margin").width @@ -86,17 +87,15 @@ Button label: Label {} } - Connections { + Connections + { target: outputDevice - onUniqueConfigurationsChanged: { - updateOnSync() - } + onUniqueConfigurationsChanged: updateOnSync() } - Connections { + Connections + { target: Cura.MachineManager - onCurrentConfigurationChanged: { - updateOnSync() - } + onCurrentConfigurationChanged: updateOnSync() } } \ No newline at end of file diff --git a/resources/qml/Sidebar.qml b/resources/qml/Sidebar.qml index 4744bbfda0..1c1bfca7c3 100644 --- a/resources/qml/Sidebar.qml +++ b/resources/qml/Sidebar.qml @@ -109,7 +109,7 @@ Rectangle ConfigurationSelection { id: configSelection - visible: isNetworkPrinter + visible: isNetworkPrinter && printerConnected width: visible ? Math.round(base.width * 0.15) : 0 height: UM.Theme.getSize("sidebar_header").height anchors.top: base.top From 36c5e0ad15365c1b48f0f64fcda7741eb6187440 Mon Sep 17 00:00:00 2001 From: Diego Prado Gesto Date: Fri, 16 Mar 2018 12:07:05 +0100 Subject: [PATCH 75/88] CURA-4870 Change message of the dialog when the connection with a printer/group already exists --- plugins/UM3NetworkPrinting/DiscoverUM3Action.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml index e7df22b546..0aaeef8fbd 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.qml @@ -56,8 +56,7 @@ Cura.MachineAction id: existingConnectionDialog title: catalog.i18nc("@window:title", "Existing Connection") icon: StandardIcon.Information - text: catalog.i18nc("@message:text", "There is an instance already connected to this group") - detailedText: catalog.i18nc("@message:description", "You can't connect two instances to the same group. Please use the other instance or connect to another group.") + text: catalog.i18nc("@message:text", "This printer/group is already added to Cura. Please select another printer/group.") standardButtons: StandardButton.Ok modality: Qt.ApplicationModal } From 8e26d27e805ad030a2623f89dcfd6f75451d5211 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 16 Mar 2018 13:15:24 +0100 Subject: [PATCH 76/88] Fix crash when clicking a non-slicable node --- plugins/SupportEraser/SupportEraser.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 2afb3dfaab..58624ea058 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -66,6 +66,9 @@ class SupportEraser(Tool): # The selection renderpass is used to identify objects in the current view self._selection_pass = Application.getInstance().getRenderer().getRenderPass("selection") picked_node = self._controller.getScene().findObject(self._selection_pass.getIdAtPosition(event.x, event.y)) + if not picked_node: + # There is no slicable object at the picked location + return node_stack = picked_node.callDecoration("getStack") if node_stack: From ee4a6dc704ed0021812bad7c7c3de7ad06dd8499 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 16 Mar 2018 13:21:22 +0100 Subject: [PATCH 77/88] Always show confirm dialog upon material diameter change --- cura/Settings/MachineManager.py | 16 ---------------- resources/qml/Preferences/MaterialView.qml | 2 +- 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 77cce9cddb..d3e85db107 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -738,22 +738,6 @@ class MachineManager(QObject): return result - ## Property to indicate if a machine has "specialized" material profiles. - # Some machines have their own material profiles that "override" the default catch all profiles. - @pyqtProperty(bool, notify = globalContainerChanged) - def filterMaterialsByMachine(self) -> bool: - if self._global_container_stack: - return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_machine_materials", False)) - return False - - ## Property to indicate if a machine has "specialized" quality profiles. - # Some machines have their own quality profiles that "override" the default catch all profiles. - @pyqtProperty(bool, notify = globalContainerChanged) - def filterQualityByMachine(self) -> bool: - if self._global_container_stack: - return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_machine_quality", False)) - return False - ## Get the Definition ID of a machine (specified by ID) # \param machine_id string machine id to get the definition ID of # \returns DefinitionID (string) if found, None otherwise diff --git a/resources/qml/Preferences/MaterialView.qml b/resources/qml/Preferences/MaterialView.qml index ffc4eaba3a..3b1c10fbbd 100644 --- a/resources/qml/Preferences/MaterialView.qml +++ b/resources/qml/Preferences/MaterialView.qml @@ -233,7 +233,7 @@ TabView var old_diameter = Cura.ContainerManager.getContainerProperty(base.containerId, "material_diameter", "value").toString(); var old_approximate_diameter = Cura.ContainerManager.getContainerMetaDataEntry(base.containerId, "approximate_diameter"); var new_approximate_diameter = getApproximateDiameter(value); - if (Cura.MachineManager.filterMaterialsByMachine && new_approximate_diameter != Cura.ExtruderManager.getActiveExtruderStack().approximateMaterialDiameter) + if (new_approximate_diameter != Cura.ExtruderManager.getActiveExtruderStack().approximateMaterialDiameter) { confirmDiameterChangeDialog.old_diameter_value = old_diameter; confirmDiameterChangeDialog.new_diameter_value = value; From 83168886d6cfafffc88ec9d7c55770f8631c3afe Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 16 Mar 2018 13:22:43 +0100 Subject: [PATCH 78/88] Parent added meshes to the parent node instead of creating a group This requires a small change in PlatformPhysics, or otherwise the added mesh would still drop down. --- cura/PlatformPhysics.py | 2 +- plugins/SupportEraser/SupportEraser.py | 41 +++----------------------- 2 files changed, 5 insertions(+), 38 deletions(-) diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 3d9d5d5027..1a5d6ef837 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -71,7 +71,7 @@ class PlatformPhysics: # Move it downwards if bottom is above platform move_vector = Vector() - if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")) and node.isEnabled(): #If an object is grouped, don't move it down + if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0 move_vector = move_vector.set(y = -bbox.bottom + z_offset) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 58624ea058..22dd9b6d9c 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -120,49 +120,16 @@ class SupportEraser(Tool): op = GroupedOperation() # First add the node to the scene, so it gets the expected transform op.addOperation(AddSceneNodeOperation(node, root)) - - # Determine the parent group the node should be put in - if parent.getParent().callDecoration("isGroup"): - group = parent.getParent() - else: - # Create a group-node - group = CuraSceneNode() - group.addDecorator(GroupDecorator()) - group.addDecorator(BuildPlateDecorator(active_build_plate)) - group.setParent(root) - center = parent.getPosition() - group.setPosition(center) - group.setCenterPosition(center) - op.addOperation(SetParentOperation(parent, group)) - - op.addOperation(SetParentOperation(node, group)) + op.addOperation(SetParentOperation(node, parent)) op.push() - Application.getInstance().getController().getScene().sceneChanged.emit(node) - # Select the picked node so the group does not get drawn as a wireframe (yet) - if not Selection.isSelected(parent): - Selection.add(parent) - if Selection.isSelected(group): - Selection.remove(group) + Application.getInstance().getController().getScene().sceneChanged.emit(node) def _removeEraserMesh(self, node: CuraSceneNode): - group = node.getParent() - if group.callDecoration("isGroup"): - parent = group.getChildren()[0] - - op = GroupedOperation() - op.addOperation(RemoveSceneNodeOperation(node)) - if len(group.getChildren()) == 2: - op.addOperation(SetParentOperation(parent, group.getParent())) - + op = RemoveSceneNodeOperation(node) op.push() - Application.getInstance().getController().getScene().sceneChanged.emit(node) - # Select the picked node so the group does not get drawn as a wireframe (yet) - if parent and not Selection.isSelected(parent): - Selection.add(parent) - if Selection.isSelected(group): - Selection.remove(group) + Application.getInstance().getController().getScene().sceneChanged.emit(node) def _updateEnabled(self): plugin_enabled = False From 78a7299fc59b8b665e49d370512442c02c2efadb Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 16 Mar 2018 14:38:56 +0100 Subject: [PATCH 79/88] Maintain a selection when removing a mesh, so the tool stays active --- plugins/SupportEraser/SupportEraser.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 22dd9b6d9c..8f89d212d0 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -126,9 +126,16 @@ class SupportEraser(Tool): Application.getInstance().getController().getScene().sceneChanged.emit(node) def _removeEraserMesh(self, node: CuraSceneNode): + parent = node.getParent() + if parent == self._controller.getScene().getRoot(): + parent = None + op = RemoveSceneNodeOperation(node) op.push() + if parent and not Selection.isSelected(parent): + Selection.add(parent) + Application.getInstance().getController().getScene().sceneChanged.emit(node) def _updateEnabled(self): From f05944bf299bb31ecb0b45f3c282329317f89d10 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 16 Mar 2018 14:47:01 +0100 Subject: [PATCH 80/88] Switch to translate tool when ctrl-clicking --- plugins/SupportEraser/SupportEraser.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 8f89d212d0..615c52ff89 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -5,6 +5,7 @@ import os import os.path from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtWidgets import QApplication from UM.Math.Vector import Vector from UM.Tool import Tool @@ -34,7 +35,7 @@ class SupportEraser(Tool): def __init__(self): super().__init__() self._shortcut_key = Qt.Key_G - self._controller = Application.getInstance().getController() + self._controller = self.getController() self._selection_pass = None Application.getInstance().globalContainerStackChanged.connect(self._updateEnabled) @@ -54,8 +55,14 @@ class SupportEraser(Tool): def event(self, event): super().event(event) + modifiers = QApplication.keyboardModifiers() + ctrl_is_active = modifiers & Qt.ControlModifier if event.type == Event.MousePressEvent and self._controller.getToolsEnabled(): + if ctrl_is_active: + self._controller.setActiveTool("TranslateTool") + return + if self._skip_press: # The selection was previously cleared, do not add/remove an anti-support mesh but # use this click for selection and reactivating this tool only. From e74191f2dbb814eab228060c5e867a9634edceac Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 16 Mar 2018 14:05:40 +0100 Subject: [PATCH 81/88] Cache preferred format I think that makes the code a bit easier to read. This is not really done to make it faster, just more semantic. Contributes to issue CURA-5097. --- .../RemovableDriveOutputDevice.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py index e719ba3286..184b6b8a20 100644 --- a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py +++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py @@ -62,14 +62,15 @@ class RemovableDriveOutputDevice(OutputDevice): if len(file_formats) == 0: Logger.log("e", "There are no file formats available to write with!") raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status", "There are no file formats available to write with!")) + preferred_format = file_formats[0] # Just take the first file format available. if file_handler is not None: - writer = file_handler.getWriterByMimeType(file_formats[0]["mime_type"]) + writer = file_handler.getWriterByMimeType(preferred_format["mime_type"]) else: - writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"]) + writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(preferred_format["mime_type"]) - extension = file_formats[0]["extension"] + extension = preferred_format["extension"] if file_name is None: file_name = self._automaticFileName(nodes) From 5a5766f11a83b189831336587133f4aa972abcdf Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 16 Mar 2018 14:37:08 +0100 Subject: [PATCH 82/88] Choose correct stream depending on output mode We need a binary stream if we're writing in a binary format. Contributes to issue CURA-5097. --- .../RemovableDriveOutputDevice.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py index 184b6b8a20..c81e4a76bc 100644 --- a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py +++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py @@ -7,7 +7,7 @@ from UM.Application import Application from UM.Logger import Logger from UM.Message import Message from UM.FileHandler.WriteFileJob import WriteFileJob -from UM.Mesh.MeshWriter import MeshWriter +from UM.FileHandler.FileWriter import FileWriter #To check against the write modes (text vs. binary). from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.OutputDevice.OutputDevice import OutputDevice from UM.OutputDevice import OutputDeviceError @@ -82,8 +82,11 @@ class RemovableDriveOutputDevice(OutputDevice): try: Logger.log("d", "Writing to %s", file_name) # Using buffering greatly reduces the write time for many lines of gcode - self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8") - job = WriteFileJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode) + if preferred_format["mode"] == FileWriter.OutputMode.TextMode: + self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8") + else: #Binary mode. + self._stream = open(file_name, "wb", buffering = 1) + job = WriteFileJob(writer, self._stream, nodes, preferred_format["mode"]) job.setFileName(file_name) job.progress.connect(self._onProgress) job.finished.connect(self._onFinished) From 4be947af5548d6131d59810f6962ed745e46b228 Mon Sep 17 00:00:00 2001 From: fieldOfView Date: Fri, 16 Mar 2018 14:55:20 +0100 Subject: [PATCH 83/88] Fix codestyle --- plugins/SupportEraser/SupportEraser.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 615c52ff89..3332b4181d 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -154,8 +154,6 @@ class SupportEraser(Tool): Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled) - - def _onSelectionChanged(self): # When selection is passed from one object to another object, first the selection is cleared # and then it is set to the new object. We are only interested in the change from no selection From 2aaaf106b40e79b4acc354cdf1f723a20391d6e6 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 16 Mar 2018 15:05:03 +0100 Subject: [PATCH 84/88] Fix update material in MachineManager CURA-5098 --- cura/Settings/MachineManager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index d3e85db107..07548e21dd 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -1109,7 +1109,7 @@ class MachineManager(QObject): from cura.Settings.CuraContainerStack import _ContainerIndexes context = PropertyEvaluationContext(extruder) context.context["evaluate_from_container_index"] = _ContainerIndexes.DefinitionChanges - material_diameter = self._global_container_stack.getProperty("material_diameter", "value", context) + material_diameter = extruder.getProperty("material_diameter", "value", context) candidate_materials = self._material_manager.getAvailableMaterials( self._global_container_stack.definition.getId(), current_variant_name, From 4d5013d3adb9ac7573eb5f62422c039c40ad6f92 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 16 Mar 2018 15:08:30 +0100 Subject: [PATCH 85/88] Remove unused variable The variable name is re-used lower in the code. This value is unused though. --- plugins/GCodeWriter/GCodeWriter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index 4b78a2a72a..ccd881afdc 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -111,7 +111,6 @@ class GCodeWriter(MeshWriter): prefix = ";SETTING_" + str(GCodeWriter.version) + " " # The prefix to put before each line. prefix_length = len(prefix) - quality_name = stack.qualityChanges.getName() quality_type = stack.quality.getMetaDataEntry("quality_type") container_with_profile = stack.qualityChanges if container_with_profile.getId() == "empty_quality_changes": From 5ca420394050bab6055ee9bb287e09b2bc04c54e Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 16 Mar 2018 15:12:26 +0100 Subject: [PATCH 86/88] Remove material types that have no fallbacks from fallback_materials_map CURA-5098 --- cura/Machines/MaterialManager.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cura/Machines/MaterialManager.py b/cura/Machines/MaterialManager.py index 0a82fcc764..b854dbf29e 100644 --- a/cura/Machines/MaterialManager.py +++ b/cura/Machines/MaterialManager.py @@ -107,6 +107,7 @@ class MaterialManager(QObject): # Map #2 # Lookup table for material type -> fallback material metadata, only for read-only materials grouped_by_type_dict = dict() + material_types_without_fallback = set() for root_material_id, material_node in self._material_group_map.items(): if not self._container_registry.isReadOnly(root_material_id): continue @@ -114,6 +115,7 @@ class MaterialManager(QObject): if material_type not in grouped_by_type_dict: grouped_by_type_dict[material_type] = {"generic": None, "others": []} + material_types_without_fallback.add(material_type) brand = material_node.root_material_node.metadata["brand"] if brand.lower() == "generic": to_add = True @@ -123,6 +125,10 @@ class MaterialManager(QObject): to_add = False # don't add if it's not the default diameter if to_add: grouped_by_type_dict[material_type] = material_node.root_material_node.metadata + material_types_without_fallback.remove(material_type) + # Remove the materials that have no fallback materials + for material_type in material_types_without_fallback: + del grouped_by_type_dict[material_type] self._fallback_materials_map = grouped_by_type_dict # Map #3 From 43ac565c20f0b65d8a8bd81a418333fdbba33d3b Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Fri, 16 Mar 2018 15:45:15 +0100 Subject: [PATCH 87/88] Fix visibility preset initialization CURA-5088 --- cura/Machines/Models/SettingVisibilityPresetsModel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cura/Machines/Models/SettingVisibilityPresetsModel.py b/cura/Machines/Models/SettingVisibilityPresetsModel.py index e281d81c39..599dd982a8 100644 --- a/cura/Machines/Models/SettingVisibilityPresetsModel.py +++ b/cura/Machines/Models/SettingVisibilityPresetsModel.py @@ -45,6 +45,8 @@ class SettingVisibilityPresetsModel(ListModel): visible_settings = self._preferences.getValue("general/visible_settings") if not visible_settings: self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item["settings"])) + else: + self._onPreferencesChanged("general/visible_settings") self.activePresetChanged.emit() From 22d3cd928f653d69e061ea0adeba810f1efebcfc Mon Sep 17 00:00:00 2001 From: Ian Paschal Date: Fri, 16 Mar 2018 15:57:19 +0100 Subject: [PATCH 88/88] CURA-5090 Remove layer view data after clearing buildplate --- plugins/SimulationView/SimulationView.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 5c3dca9fae..456d64e250 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -158,7 +158,9 @@ class SimulationView(View): return self._nozzle_node def _onSceneChanged(self, node): - if node.getMeshData() is not None: + if node.getMeshData() is None: + self.resetLayerData() + else: self.setActivity(False) self.calculateMaxLayers() self.calculateMaxPathsOnLayer(self._current_layer_num)