diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py new file mode 100644 index 0000000000..657e5c5387 --- /dev/null +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -0,0 +1,206 @@ +# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V. +# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. +from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot + +from UM.PluginRegistry import PluginRegistry +from UM.Resources import Resources +from UM.Application import Application +from UM.Extension import Extension +from UM.Logger import Logger + +import os.path +import pkgutil +import sys +import importlib.util + +from UM.i18n import i18nCatalog +i18n_catalog = i18nCatalog("cura") + + +## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated +# g-code files. +class PostProcessingPlugin(QObject, Extension): + def __init__(self, parent = None): + super().__init__(parent) + self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup) + self._view = None + + # Loaded scripts are all scripts that can be used + self._loaded_scripts = {} + self._script_labels = {} + + # Script list contains instances of scripts in loaded_scripts. + # There can be duplicates, which will be executed in sequence. + self._script_list = [] + self._selected_script_index = -1 + + Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute) + + selectedIndexChanged = pyqtSignal() + @pyqtProperty("QVariant", notify = selectedIndexChanged) + def selectedScriptDefinitionId(self): + try: + return self._script_list[self._selected_script_index].getDefinitionId() + except: + return "" + + @pyqtProperty("QVariant", notify=selectedIndexChanged) + def selectedScriptStackId(self): + try: + return self._script_list[self._selected_script_index].getStackId() + except: + return "" + + ## Execute all post-processing scripts on the gcode. + def execute(self, output_device): + scene = Application.getInstance().getController().getScene() + gcode_dict = getattr(scene, "gcode_dict") + if not gcode_dict: + return + + # get gcode list for the active build plate + active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate + gcode_list = gcode_dict[active_build_plate_id] + if not gcode_list: + return + + if ";POSTPROCESSED" not in gcode_list[0]: + for script in self._script_list: + try: + gcode_list = script.execute(gcode_list) + except Exception: + Logger.logException("e", "Exception in post-processing script.") + if len(self._script_list): # Add comment to g-code if any changes were made. + gcode_list[0] += ";POSTPROCESSED\n" + gcode_dict[active_build_plate_id] = gcode_list + setattr(scene, "gcode_dict", gcode_dict) + else: + Logger.log("e", "Already post processed") + + @pyqtSlot(int) + def setSelectedScriptIndex(self, index): + self._selected_script_index = index + self.selectedIndexChanged.emit() + + @pyqtProperty(int, notify = selectedIndexChanged) + def selectedScriptIndex(self): + return self._selected_script_index + + @pyqtSlot(int, int) + def moveScript(self, index, new_index): + if new_index < 0 or new_index > len(self._script_list) - 1: + return # nothing needs to be done + else: + # Magical switch code. + self._script_list[new_index], self._script_list[index] = self._script_list[index], self._script_list[new_index] + self.scriptListChanged.emit() + self.selectedIndexChanged.emit() #Ensure that settings are updated + self._propertyChanged() + + ## Remove a script from the active script list by index. + @pyqtSlot(int) + def removeScriptByIndex(self, index): + self._script_list.pop(index) + if len(self._script_list) - 1 < self._selected_script_index: + self._selected_script_index = len(self._script_list) - 1 + self.scriptListChanged.emit() + self.selectedIndexChanged.emit() # Ensure that settings are updated + self._propertyChanged() + + ## Load all scripts from provided path. + # This should probably only be done on init. + # \param path Path to check for scripts. + def loadAllScripts(self, path): + scripts = pkgutil.iter_modules(path = [path]) + for loader, script_name, ispkg in scripts: + # Iterate over all scripts. + if script_name not in sys.modules: + spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py")) + loaded_script = importlib.util.module_from_spec(spec) + spec.loader.exec_module(loaded_script) + sys.modules[script_name] = loaded_script + + loaded_class = getattr(loaded_script, script_name) + temp_object = loaded_class() + Logger.log("d", "Begin loading of script: %s", script_name) + try: + setting_data = temp_object.getSettingData() + if "name" in setting_data and "key" in setting_data: + self._script_labels[setting_data["key"]] = setting_data["name"] + self._loaded_scripts[setting_data["key"]] = loaded_class + else: + Logger.log("w", "Script %s.py has no name or key", script_name) + self._script_labels[script_name] = script_name + self._loaded_scripts[script_name] = loaded_class + except AttributeError: + Logger.log("e", "Script %s.py is not a recognised script type. Ensure it inherits Script", script_name) + except NotImplementedError: + Logger.log("e", "Script %s.py has no implemented settings", script_name) + self.loadedScriptListChanged.emit() + + loadedScriptListChanged = pyqtSignal() + @pyqtProperty("QVariantList", notify = loadedScriptListChanged) + def loadedScriptList(self): + return sorted(list(self._loaded_scripts.keys())) + + @pyqtSlot(str, result = str) + def getScriptLabelByKey(self, key): + return self._script_labels[key] + + scriptListChanged = pyqtSignal() + @pyqtProperty("QVariantList", notify = scriptListChanged) + def scriptList(self): + script_list = [script.getSettingData()["key"] for script in self._script_list] + return script_list + + @pyqtSlot(str) + def addScriptToList(self, key): + Logger.log("d", "Adding script %s to list.", key) + new_script = self._loaded_scripts[key]() + self._script_list.append(new_script) + self.setSelectedScriptIndex(len(self._script_list) - 1) + self.scriptListChanged.emit() + self._propertyChanged() + + ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. + def _createView(self): + Logger.log("d", "Creating post processing plugin view.") + + ## Load all scripts in the scripts folders + for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Preferences)]: + try: + path = os.path.join(root, "scripts") + if not os.path.isdir(path): + try: + os.makedirs(path) + except OSError: + Logger.log("w", "Unable to create a folder for scripts: " + path) + continue + + self.loadAllScripts(path) + except Exception as e: + Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e))) + + # Create the plugin dialog component + path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml") + self._view = Application.getInstance().createQmlComponent(path, {"manager": self}) + Logger.log("d", "Post processing view created.") + + # Create the save button component + Application.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton")) + + ## Show the (GUI) popup of the post processing plugin. + def showPopup(self): + if self._view is None: + self._createView() + self._view.show() + + ## Property changed: trigger re-slice + # To do this we use the global container stack propertyChanged. + # Re-slicing is necessary for setting changes in this plugin, because the changes + # are applied only once per "fresh" gcode + def _propertyChanged(self): + global_container_stack = Application.getInstance().getGlobalContainerStack() + global_container_stack.propertyChanged.emit("post_processing_plugin", "value") + + diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml new file mode 100644 index 0000000000..d64d60a04a --- /dev/null +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml @@ -0,0 +1,501 @@ +// Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V. +// The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. + +import QtQuick 2.2 +import QtQuick.Controls 1.1 +import QtQuick.Controls.Styles 1.1 +import QtQuick.Layouts 1.1 +import QtQuick.Dialogs 1.1 +import QtQuick.Window 2.2 + +import UM 1.2 as UM +import Cura 1.0 as Cura + +UM.Dialog +{ + id: dialog + + title: catalog.i18nc("@title:window", "Post Processing Plugin") + width: 700 * screenScaleFactor; + height: 500 * screenScaleFactor; + minimumWidth: 400 * screenScaleFactor; + minimumHeight: 250 * screenScaleFactor; + + Item + { + UM.I18nCatalog{id: catalog; name:"cura"} + id: base + property int columnWidth: Math.floor((base.width / 2) - UM.Theme.getSize("default_margin").width) + property int textMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2) + property string activeScriptName + SystemPalette{ id: palette } + SystemPalette{ id: disabledPalette; colorGroup: SystemPalette.Disabled } + anchors.fill: parent + + ExclusiveGroup + { + id: selectedScriptGroup + } + Item + { + id: activeScripts + anchors.left: parent.left + width: base.columnWidth + height: parent.height + + Label + { + id: activeScriptsHeader + text: catalog.i18nc("@label", "Post Processing Scripts") + anchors.top: parent.top + anchors.topMargin: base.textMargin + anchors.left: parent.left + anchors.leftMargin: base.textMargin + anchors.right: parent.right + anchors.rightMargin: base.textMargin + font: UM.Theme.getFont("large") + } + ListView + { + id: activeScriptsList + anchors.top: activeScriptsHeader.bottom + anchors.topMargin: base.textMargin + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: parent.right + anchors.rightMargin: base.textMargin + height: childrenRect.height + model: manager.scriptList + delegate: Item + { + width: parent.width + height: activeScriptButton.height + Button + { + id: activeScriptButton + text: manager.getScriptLabelByKey(modelData.toString()) + exclusiveGroup: selectedScriptGroup + checkable: true + checked: { + if (manager.selectedScriptIndex == index) + { + base.activeScriptName = manager.getScriptLabelByKey(modelData.toString()) + return true + } + else + { + return false + } + } + onClicked: + { + forceActiveFocus() + manager.setSelectedScriptIndex(index) + base.activeScriptName = manager.getScriptLabelByKey(modelData.toString()) + } + width: parent.width + height: UM.Theme.getSize("setting").height + style: ButtonStyle + { + background: Rectangle + { + color: activeScriptButton.checked ? palette.highlight : "transparent" + width: parent.width + height: parent.height + } + label: Label + { + wrapMode: Text.Wrap + text: control.text + color: activeScriptButton.checked ? palette.highlightedText : palette.text + } + } + } + Button + { + id: removeButton + text: "x" + width: 20 * screenScaleFactor + height: 20 * screenScaleFactor + anchors.right:parent.right + anchors.rightMargin: base.textMargin + anchors.verticalCenter: parent.verticalCenter + onClicked: manager.removeScriptByIndex(index) + style: ButtonStyle + { + label: Item + { + UM.RecolorImage + { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: Math.floor(control.width / 2.7) + height: Math.floor(control.height / 2.7) + sourceSize.width: width + sourceSize.height: width + color: palette.text + source: UM.Theme.getIcon("cross1") + } + } + } + } + Button + { + id: downButton + text: "" + anchors.right: removeButton.left + anchors.verticalCenter: parent.verticalCenter + enabled: index != manager.scriptList.length - 1 + width: 20 * screenScaleFactor + height: 20 * screenScaleFactor + onClicked: + { + if (manager.selectedScriptIndex == index) + { + manager.setSelectedScriptIndex(index + 1) + } + return manager.moveScript(index, index + 1) + } + style: ButtonStyle + { + label: Item + { + UM.RecolorImage + { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: Math.floor(control.width / 2.5) + height: Math.floor(control.height / 2.5) + sourceSize.width: width + sourceSize.height: width + color: control.enabled ? palette.text : disabledPalette.text + source: UM.Theme.getIcon("arrow_bottom") + } + } + } + } + Button + { + id: upButton + text: "" + enabled: index != 0 + width: 20 * screenScaleFactor + height: 20 * screenScaleFactor + anchors.right: downButton.left + anchors.verticalCenter: parent.verticalCenter + onClicked: + { + if (manager.selectedScriptIndex == index) + { + manager.setSelectedScriptIndex(index - 1) + } + return manager.moveScript(index, index - 1) + } + style: ButtonStyle + { + label: Item + { + UM.RecolorImage + { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: Math.floor(control.width / 2.5) + height: Math.floor(control.height / 2.5) + sourceSize.width: width + sourceSize.height: width + color: control.enabled ? palette.text : disabledPalette.text + source: UM.Theme.getIcon("arrow_top") + } + } + } + } + } + } + Button + { + id: addButton + text: catalog.i18nc("@action", "Add a script") + anchors.left: parent.left + anchors.leftMargin: base.textMargin + anchors.top: activeScriptsList.bottom + anchors.topMargin: base.textMargin + menu: scriptsMenu + style: ButtonStyle + { + label: Label + { + text: control.text + } + } + } + Menu + { + id: scriptsMenu + + Instantiator + { + model: manager.loadedScriptList + + MenuItem + { + text: manager.getScriptLabelByKey(modelData.toString()) + onTriggered: manager.addScriptToList(modelData.toString()) + } + + onObjectAdded: scriptsMenu.insertItem(index, object); + onObjectRemoved: scriptsMenu.removeItem(object); + } + } + } + + Rectangle + { + color: UM.Theme.getColor("sidebar") + anchors.left: activeScripts.right + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: parent.right + height: parent.height + id: settingsPanel + + Label + { + id: scriptSpecsHeader + text: manager.selectedScriptIndex == -1 ? catalog.i18nc("@label", "Settings") : base.activeScriptName + anchors.top: parent.top + anchors.topMargin: base.textMargin + anchors.left: parent.left + anchors.leftMargin: base.textMargin + anchors.right: parent.right + anchors.rightMargin: base.textMargin + height: 20 * screenScaleFactor + font: UM.Theme.getFont("large") + color: UM.Theme.getColor("text") + } + + ScrollView + { + id: scrollView + anchors.top: scriptSpecsHeader.bottom + anchors.topMargin: settingsPanel.textMargin + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + visible: manager.selectedScriptDefinitionId != "" + style: UM.Theme.styles.scrollview; + + ListView + { + id: listview + spacing: UM.Theme.getSize("default_lining").height + model: UM.SettingDefinitionsModel + { + id: definitionsModel; + containerId: manager.selectedScriptDefinitionId + showAll: true + } + delegate:Loader + { + id: settingLoader + + width: parent.width + height: + { + if(provider.properties.enabled == "True") + { + if(model.type != undefined) + { + return UM.Theme.getSize("section").height; + } + else + { + return 0; + } + } + else + { + return 0; + } + + } + Behavior on height { NumberAnimation { duration: 100 } } + opacity: provider.properties.enabled == "True" ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: 100 } } + enabled: opacity > 0 + property var definition: model + property var settingDefinitionsModel: definitionsModel + property var propertyProvider: provider + property var globalPropertyProvider: inheritStackProvider + + //Qt5.4.2 and earlier has a bug where this causes a crash: https://bugreports.qt.io/browse/QTBUG-35989 + //In addition, while it works for 5.5 and higher, the ordering of the actual combo box drop down changes, + //causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely. + asynchronous: model.type != "enum" && model.type != "extruder" + + onLoaded: { + settingLoader.item.showRevertButton = false + settingLoader.item.showInheritButton = false + settingLoader.item.showLinkedSettingIcon = false + settingLoader.item.doDepthIndentation = true + settingLoader.item.doQualityUserSettingEmphasis = false + } + + sourceComponent: + { + switch(model.type) + { + case "int": + return settingTextField + case "float": + return settingTextField + case "enum": + return settingComboBox + case "extruder": + return settingExtruder + case "bool": + return settingCheckBox + case "str": + return settingTextField + case "category": + return settingCategory + default: + return settingUnknown + } + } + + UM.SettingPropertyProvider + { + id: provider + containerStackId: manager.selectedScriptStackId + key: model.key ? model.key : "None" + watchedProperties: [ "value", "enabled", "state", "validationState" ] + storeIndex: 0 + } + + // Specialty provider that only watches global_inherits (we cant filter on what property changed we get events + // so we bypass that to make a dedicated provider). + UM.SettingPropertyProvider + { + id: inheritStackProvider + containerStackId: Cura.MachineManager.activeMachineId + key: model.key ? model.key : "None" + watchedProperties: [ "limit_to_extruder" ] + } + + Connections + { + target: item + + onShowTooltip: + { + tooltip.text = text; + var position = settingLoader.mapToItem(settingsPanel, settingsPanel.x, 0); + tooltip.show(position); + tooltip.target.x = position.x + 1 + } + + onHideTooltip: + { + tooltip.hide(); + } + } + + } + } + } + } + + Cura.SidebarTooltip + { + id: tooltip + } + + Component + { + id: settingTextField; + + Cura.SettingTextField { } + } + + Component + { + id: settingComboBox; + + Cura.SettingComboBox { } + } + + Component + { + id: settingExtruder; + + Cura.SettingExtruder { } + } + + Component + { + id: settingCheckBox; + + Cura.SettingCheckBox { } + } + + Component + { + id: settingCategory; + + Cura.SettingCategory { } + } + + Component + { + id: settingUnknown; + + Cura.SettingUnknown { } + } + } + rightButtons: Button + { + text: catalog.i18nc("@action:button", "Close") + iconName: "dialog-close" + onClicked: dialog.accept() + } + + Button { + objectName: "postProcessingSaveAreaButton" + visible: activeScriptsList.count > 0 + height: UM.Theme.getSize("save_button_save_to_button").height + width: height + tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts") + onClicked: dialog.show() + + style: ButtonStyle { + background: Rectangle { + id: deviceSelectionIcon + border.width: UM.Theme.getSize("default_lining").width + border.color: !control.enabled ? UM.Theme.getColor("action_button_disabled_border") : + 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.enabled ? UM.Theme.getColor("action_button_disabled") : + 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; } } + anchors.left: parent.left + anchors.leftMargin: Math.floor(UM.Theme.getSize("save_button_text_margin").width / 2); + width: parent.height + height: parent.height + + UM.RecolorImage { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: Math.floor(parent.width / 2) + height: Math.floor(parent.height / 2) + sourceSize.width: width + sourceSize.height: height + color: !control.enabled ? UM.Theme.getColor("action_button_disabled_text") : + control.pressed ? UM.Theme.getColor("action_button_active_text") : + control.hovered ? UM.Theme.getColor("action_button_hovered_text") : UM.Theme.getColor("action_button_text"); + source: "postprocessing.svg" + } + } + label: Label{ } + } + } +} \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/README.md b/plugins/PostProcessingPlugin/README.md new file mode 100644 index 0000000000..988f40007d --- /dev/null +++ b/plugins/PostProcessingPlugin/README.md @@ -0,0 +1,2 @@ +# PostProcessingPlugin +A post processing plugin for Cura diff --git a/plugins/PostProcessingPlugin/Script.py b/plugins/PostProcessingPlugin/Script.py new file mode 100644 index 0000000000..7d603ba11f --- /dev/null +++ b/plugins/PostProcessingPlugin/Script.py @@ -0,0 +1,111 @@ +# Copyright (c) 2015 Jaime van Kessel +# Copyright (c) 2017 Ultimaker B.V. +# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. +from UM.Logger import Logger +from UM.Signal import Signal, signalemitter +from UM.i18n import i18nCatalog + +# Setting stuff import +from UM.Application import Application +from UM.Settings.ContainerStack import ContainerStack +from UM.Settings.InstanceContainer import InstanceContainer +from UM.Settings.DefinitionContainer import DefinitionContainer +from UM.Settings.ContainerRegistry import ContainerRegistry + +import re +import json +import collections +i18n_catalog = i18nCatalog("cura") + + +## Base class for scripts. All scripts should inherit the script class. +@signalemitter +class Script: + def __init__(self): + super().__init__() + self._settings = None + self._stack = None + + setting_data = self.getSettingData() + self._stack = ContainerStack(stack_id = str(id(self))) + self._stack.setDirty(False) # This stack does not need to be saved. + + + ## Check if the definition of this script already exists. If not, add it to the registry. + if "key" in setting_data: + definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = setting_data["key"]) + if definitions: + # Definition was found + self._definition = definitions[0] + else: + self._definition = DefinitionContainer(setting_data["key"]) + self._definition.deserialize(json.dumps(setting_data)) + ContainerRegistry.getInstance().addContainer(self._definition) + self._stack.addContainer(self._definition) + self._instance = InstanceContainer(container_id="ScriptInstanceContainer") + self._instance.setDefinition(self._definition.getId()) + self._instance.addMetaDataEntry("setting_version", self._definition.getMetaDataEntry("setting_version", default = 0)) + self._stack.addContainer(self._instance) + self._stack.propertyChanged.connect(self._onPropertyChanged) + + ContainerRegistry.getInstance().addContainer(self._stack) + + settingsLoaded = Signal() + valueChanged = Signal() # Signal emitted whenever a value of a setting is changed + + def _onPropertyChanged(self, key, property_name): + if property_name == "value": + self.valueChanged.emit() + + # Property changed: trigger reslice + # To do this we use the global container stack propertyChanged. + # Reslicing is necessary for setting changes in this plugin, because the changes + # are applied only once per "fresh" gcode + global_container_stack = Application.getInstance().getGlobalContainerStack() + global_container_stack.propertyChanged.emit(key, property_name) + + ## Needs to return a dict that can be used to construct a settingcategory file. + # See the example script for an example. + # It follows the same style / guides as the Uranium settings. + # Scripts can either override getSettingData directly, or use getSettingDataString + # to return a string that will be parsed as json. The latter has the benefit over + # returning a dict in that the order of settings is maintained. + def getSettingData(self): + setting_data = self.getSettingDataString() + if type(setting_data) == str: + setting_data = json.loads(setting_data, object_pairs_hook = collections.OrderedDict) + return setting_data + + def getSettingDataString(self): + raise NotImplementedError() + + def getDefinitionId(self): + if self._stack: + return self._stack.getBottom().getId() + + def getStackId(self): + if self._stack: + return self._stack.getId() + + ## Convenience function that retrieves value of a setting from the stack. + def getSettingValueByKey(self, key): + return self._stack.getProperty(key, "value") + + ## Convenience function that finds the value in a line of g-code. + # When requesting key = x from line "G1 X100" the value 100 is returned. + def getValue(self, line, key, default = None): + if not key in line or (';' in line and line.find(key) > line.find(';')): + return default + sub_part = line[line.find(key) + 1:] + m = re.search('^-?[0-9]+\.?[0-9]*', sub_part) + if m is None: + return default + try: + return float(m.group(0)) + except: + return default + + ## This is called when the script is executed. + # It gets a list of g-code strings and needs to return a (modified) list. + def execute(self, data): + raise NotImplementedError() diff --git a/plugins/PostProcessingPlugin/__init__.py b/plugins/PostProcessingPlugin/__init__.py new file mode 100644 index 0000000000..85f1126136 --- /dev/null +++ b/plugins/PostProcessingPlugin/__init__.py @@ -0,0 +1,11 @@ +# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V. +# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. + +from . import PostProcessingPlugin +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") +def getMetaData(): + return {} + +def register(app): + return {"extension": PostProcessingPlugin.PostProcessingPlugin()} \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/plugin.json b/plugins/PostProcessingPlugin/plugin.json new file mode 100644 index 0000000000..ebfef8145a --- /dev/null +++ b/plugins/PostProcessingPlugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "Post Processing", + "author": "Ultimaker", + "version": "2.2", + "api": 4, + "description": "Extension that allows for user created scripts for post processing", + "catalog": "cura" +} \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/postprocessing.svg b/plugins/PostProcessingPlugin/postprocessing.svg new file mode 100644 index 0000000000..f55face4a9 --- /dev/null +++ b/plugins/PostProcessingPlugin/postprocessing.svg @@ -0,0 +1,47 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py b/plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py new file mode 100644 index 0000000000..fb59378206 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py @@ -0,0 +1,48 @@ +from ..Script import Script +class BQ_PauseAtHeight(Script): + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"Pause at height (BQ Printers)", + "key": "BQ_PauseAtHeight", + "metadata":{}, + "version": 2, + "settings": + { + "pause_height": + { + "label": "Pause height", + "description": "At what height should the pause occur", + "unit": "mm", + "type": "float", + "default_value": 5.0 + } + } + }""" + + def execute(self, data): + x = 0. + y = 0. + current_z = 0. + pause_z = self.getSettingValueByKey("pause_height") + for layer in data: + lines = layer.split("\n") + for line in lines: + if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0: + current_z = self.getValue(line, 'Z') + if current_z != None: + if current_z >= pause_z: + prepend_gcode = ";TYPE:CUSTOM\n" + prepend_gcode += "; -- Pause at height (%.2f mm) --\n" % pause_z + + # Insert Pause gcode + prepend_gcode += "M25 ; Pauses the print and waits for the user to resume it\n" + + index = data.index(layer) + layer = prepend_gcode + layer + data[index] = layer # Override the data of this layer with the modified data + return data + break + return data diff --git a/plugins/PostProcessingPlugin/scripts/ColorChange.py b/plugins/PostProcessingPlugin/scripts/ColorChange.py new file mode 100644 index 0000000000..8db45f4033 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/ColorChange.py @@ -0,0 +1,76 @@ +# This PostProcessing Plugin script is released +# under the terms of the AGPLv3 or higher + +from ..Script import Script +#from UM.Logger import Logger +# from cura.Settings.ExtruderManager import ExtruderManager + +class ColorChange(Script): + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"Color Change", + "key": "ColorChange", + "metadata": {}, + "version": 2, + "settings": + { + "layer_number": + { + "label": "Layer", + "description": "At what layer should color change occur. This will be before the layer starts printing. Specify multiple color changes with a comma.", + "unit": "", + "type": "str", + "default_value": "1" + }, + + "initial_retract": + { + "label": "Initial Retraction", + "description": "Initial filament retraction distance", + "unit": "mm", + "type": "float", + "default_value": 300.0 + }, + "later_retract": + { + "label": "Later Retraction Distance", + "description": "Later filament retraction distance for removal", + "unit": "mm", + "type": "float", + "default_value": 30.0 + } + } + }""" + + def execute(self, data: list): + + """data is a list. Each index contains a layer""" + layer_nums = self.getSettingValueByKey("layer_number") + initial_retract = self.getSettingValueByKey("initial_retract") + later_retract = self.getSettingValueByKey("later_retract") + + color_change = "M600" + + if initial_retract is not None and initial_retract > 0.: + color_change = color_change + (" E%.2f" % initial_retract) + + if later_retract is not None and later_retract > 0.: + color_change = color_change + (" L%.2f" % later_retract) + + color_change = color_change + " ; Generated by ColorChange plugin" + + layer_targets = layer_nums.split(',') + if len(layer_targets) > 0: + for layer_num in layer_targets: + layer_num = int( layer_num.strip() ) + if layer_num < len(data): + layer = data[ layer_num - 1 ] + lines = layer.split("\n") + lines.insert(2, color_change ) + final_line = "\n".join( lines ) + data[ layer_num - 1 ] = final_line + + return data diff --git a/plugins/PostProcessingPlugin/scripts/ExampleScript.py b/plugins/PostProcessingPlugin/scripts/ExampleScript.py new file mode 100644 index 0000000000..416a5f5404 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/ExampleScript.py @@ -0,0 +1,43 @@ +# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V. +# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. +from ..Script import Script + +class ExampleScript(Script): + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"Example script", + "key": "ExampleScript", + "metadata": {}, + "version": 2, + "settings": + { + "test": + { + "label": "Test", + "description": "None", + "unit": "mm", + "type": "float", + "default_value": 0.5, + "minimum_value": "0", + "minimum_value_warning": "0.1", + "maximum_value_warning": "1" + }, + "derp": + { + "label": "zomg", + "description": "afgasgfgasfgasf", + "unit": "mm", + "type": "float", + "default_value": 0.5, + "minimum_value": "0", + "minimum_value_warning": "0.1", + "maximum_value_warning": "1" + } + } + }""" + + def execute(self, data): + return data \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py new file mode 100644 index 0000000000..925a5a7ac5 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py @@ -0,0 +1,221 @@ +from ..Script import Script +# from cura.Settings.ExtruderManager import ExtruderManager + +class PauseAtHeight(Script): + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"Pause at height", + "key": "PauseAtHeight", + "metadata": {}, + "version": 2, + "settings": + { + "pause_height": + { + "label": "Pause Height", + "description": "At what height should the pause occur", + "unit": "mm", + "type": "float", + "default_value": 5.0 + }, + "head_park_x": + { + "label": "Park Print Head X", + "description": "What X location does the head move to when pausing.", + "unit": "mm", + "type": "float", + "default_value": 190 + }, + "head_park_y": + { + "label": "Park Print Head Y", + "description": "What Y location does the head move to when pausing.", + "unit": "mm", + "type": "float", + "default_value": 190 + }, + "retraction_amount": + { + "label": "Retraction", + "description": "How much filament must be retracted at pause.", + "unit": "mm", + "type": "float", + "default_value": 0 + }, + "retraction_speed": + { + "label": "Retraction Speed", + "description": "How fast to retract the filament.", + "unit": "mm/s", + "type": "float", + "default_value": 25 + }, + "extrude_amount": + { + "label": "Extrude Amount", + "description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.", + "unit": "mm", + "type": "float", + "default_value": 0 + }, + "extrude_speed": + { + "label": "Extrude Speed", + "description": "How fast to extrude the material after pause.", + "unit": "mm/s", + "type": "float", + "default_value": 3.3333 + }, + "redo_layers": + { + "label": "Redo Layers", + "description": "Redo a number of previous layers after a pause to increases adhesion.", + "unit": "layers", + "type": "int", + "default_value": 0 + }, + "standby_temperature": + { + "label": "Standby Temperature", + "description": "Change the temperature during the pause", + "unit": "°C", + "type": "int", + "default_value": 0 + }, + "resume_temperature": + { + "label": "Resume Temperature", + "description": "Change the temperature after the pause", + "unit": "°C", + "type": "int", + "default_value": 0 + } + } + }""" + + def execute(self, data: list): + + """data is a list. Each index contains a layer""" + + x = 0. + y = 0. + current_z = 0. + pause_height = self.getSettingValueByKey("pause_height") + retraction_amount = self.getSettingValueByKey("retraction_amount") + retraction_speed = self.getSettingValueByKey("retraction_speed") + extrude_amount = self.getSettingValueByKey("extrude_amount") + extrude_speed = self.getSettingValueByKey("extrude_speed") + park_x = self.getSettingValueByKey("head_park_x") + park_y = self.getSettingValueByKey("head_park_y") + layers_started = False + redo_layers = self.getSettingValueByKey("redo_layers") + standby_temperature = self.getSettingValueByKey("standby_temperature") + resume_temperature = self.getSettingValueByKey("resume_temperature") + + # T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value") + # with open("out.txt", "w") as f: + # f.write(T) + + # use offset to calculate the current height: = - + layer_0_z = 0. + got_first_g_cmd_on_layer_0 = False + for layer in data: + lines = layer.split("\n") + for line in lines: + if ";LAYER:0" in line: + layers_started = True + continue + + if not layers_started: + continue + + if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0: + current_z = self.getValue(line, 'Z') + if not got_first_g_cmd_on_layer_0: + layer_0_z = current_z + got_first_g_cmd_on_layer_0 = True + + x = self.getValue(line, 'X', x) + y = self.getValue(line, 'Y', y) + if current_z is not None: + current_height = current_z - layer_0_z + if current_height >= pause_height: + index = data.index(layer) + prevLayer = data[index - 1] + prevLines = prevLayer.split("\n") + current_e = 0. + for prevLine in reversed(prevLines): + current_e = self.getValue(prevLine, 'E', -1) + if current_e >= 0: + break + + # include a number of previous layers + for i in range(1, redo_layers + 1): + prevLayer = data[index - i] + layer = prevLayer + layer + + prepend_gcode = ";TYPE:CUSTOM\n" + prepend_gcode += ";added code by post processing\n" + prepend_gcode += ";script: PauseAtHeight.py\n" + prepend_gcode += ";current z: %f \n" % current_z + prepend_gcode += ";current height: %f \n" % current_height + + # Retraction + prepend_gcode += "M83\n" + if retraction_amount != 0: + prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60) + + # Move the head away + prepend_gcode += "G1 Z%f F300\n" % (current_z + 1) + prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y) + if current_z < 15: + prepend_gcode += "G1 Z15 F300\n" + + # Disable the E steppers + prepend_gcode += "M84 E0\n" + + # Set extruder standby temperature + prepend_gcode += "M104 S%i; standby temperature\n" % (standby_temperature) + + # Wait till the user continues printing + prepend_gcode += "M0 ;Do the actual pause\n" + + # Set extruder resume temperature + prepend_gcode += "M109 S%i; resume temperature\n" % (resume_temperature) + + # Push the filament back, + if retraction_amount != 0: + prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60) + + # Optionally extrude material + if extrude_amount != 0: + prepend_gcode += "G1 E%f F%f\n" % (extrude_amount, extrude_speed * 60) + + # and retract again, the properly primes the nozzle + # when changing filament. + if retraction_amount != 0: + prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60) + + # Move the head back + prepend_gcode += "G1 Z%f F300\n" % (current_z + 1) + prepend_gcode += "G1 X%f Y%f F9000\n" % (x, y) + if retraction_amount != 0: + prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60) + prepend_gcode += "G1 F9000\n" + prepend_gcode += "M82\n" + + # reset extrude value to pre pause value + prepend_gcode += "G92 E%f\n" % (current_e) + + layer = prepend_gcode + layer + + + # Override the data of this layer with the + # modified data + data[index] = layer + return data + break + return data diff --git a/plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py b/plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py new file mode 100644 index 0000000000..710baab26a --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py @@ -0,0 +1,169 @@ +from ..Script import Script +class PauseAtHeightforRepetier(Script): + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"Pause at height for repetier", + "key": "PauseAtHeightforRepetier", + "metadata": {}, + "version": 2, + "settings": + { + "pause_height": + { + "label": "Pause height", + "description": "At what height should the pause occur", + "unit": "mm", + "type": "float", + "default_value": 5.0 + }, + "head_park_x": + { + "label": "Park print head X", + "description": "What x location does the head move to when pausing.", + "unit": "mm", + "type": "float", + "default_value": 5.0 + }, + "head_park_y": + { + "label": "Park print head Y", + "description": "What y location does the head move to when pausing.", + "unit": "mm", + "type": "float", + "default_value": 5.0 + }, + "head_move_Z": + { + "label": "Head move Z", + "description": "The Hieght of Z-axis retraction before parking.", + "unit": "mm", + "type": "float", + "default_value": 15.0 + }, + "retraction_amount": + { + "label": "Retraction", + "description": "How much fillament must be retracted at pause.", + "unit": "mm", + "type": "float", + "default_value": 5.0 + }, + "extrude_amount": + { + "label": "Extrude amount", + "description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.", + "unit": "mm", + "type": "float", + "default_value": 90.0 + }, + "redo_layers": + { + "label": "Redo layers", + "description": "Redo a number of previous layers after a pause to increases adhesion.", + "unit": "layers", + "type": "int", + "default_value": 0 + } + } + }""" + + def execute(self, data): + x = 0. + y = 0. + current_z = 0. + pause_z = self.getSettingValueByKey("pause_height") + retraction_amount = self.getSettingValueByKey("retraction_amount") + extrude_amount = self.getSettingValueByKey("extrude_amount") + park_x = self.getSettingValueByKey("head_park_x") + park_y = self.getSettingValueByKey("head_park_y") + move_Z = self.getSettingValueByKey("head_move_Z") + layers_started = False + redo_layers = self.getSettingValueByKey("redo_layers") + for layer in data: + lines = layer.split("\n") + for line in lines: + if ";LAYER:0" in line: + layers_started = True + continue + + if not layers_started: + continue + + if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0: + current_z = self.getValue(line, 'Z') + x = self.getValue(line, 'X', x) + y = self.getValue(line, 'Y', y) + if current_z != None: + if current_z >= pause_z: + + index = data.index(layer) + prevLayer = data[index-1] + prevLines = prevLayer.split("\n") + current_e = 0. + for prevLine in reversed(prevLines): + current_e = self.getValue(prevLine, 'E', -1) + if current_e >= 0: + break + + prepend_gcode = ";TYPE:CUSTOM\n" + prepend_gcode += ";added code by post processing\n" + prepend_gcode += ";script: PauseAtHeightforRepetier.py\n" + prepend_gcode += ";current z: %f \n" % (current_z) + prepend_gcode += ";current X: %f \n" % (x) + prepend_gcode += ";current Y: %f \n" % (y) + + #Retraction + prepend_gcode += "M83\n" + if retraction_amount != 0: + prepend_gcode += "G1 E-%f F6000\n" % (retraction_amount) + + #Move the head away + prepend_gcode += "G1 Z%f F300\n" % (1 + current_z) + prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y) + if current_z < move_Z: + prepend_gcode += "G1 Z%f F300\n" % (current_z + move_Z) + + #Disable the E steppers + prepend_gcode += "M84 E0\n" + #Wait till the user continues printing + prepend_gcode += "@pause now change filament and press continue printing ;Do the actual pause\n" + + #Push the filament back, + if retraction_amount != 0: + prepend_gcode += "G1 E%f F6000\n" % (retraction_amount) + + # Optionally extrude material + if extrude_amount != 0: + prepend_gcode += "G1 E%f F200\n" % (extrude_amount) + prepend_gcode += "@info wait for cleaning nozzle from previous filament\n" + prepend_gcode += "@pause remove the waste filament from parking area and press continue printing\n" + + # and retract again, the properly primes the nozzle when changing filament. + if retraction_amount != 0: + prepend_gcode += "G1 E-%f F6000\n" % (retraction_amount) + + #Move the head back + prepend_gcode += "G1 Z%f F300\n" % (1 + current_z) + prepend_gcode +="G1 X%f Y%f F9000\n" % (x, y) + if retraction_amount != 0: + prepend_gcode +="G1 E%f F6000\n" % (retraction_amount) + prepend_gcode +="G1 F9000\n" + prepend_gcode +="M82\n" + + # reset extrude value to pre pause value + prepend_gcode +="G92 E%f\n" % (current_e) + + layer = prepend_gcode + layer + + # include a number of previous layers + for i in range(1, redo_layers + 1): + prevLayer = data[index-i] + layer = prevLayer + layer + + data[index] = layer #Override the data of this layer with the modified data + return data + break + return data diff --git a/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py b/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py new file mode 100644 index 0000000000..68d697e470 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py @@ -0,0 +1,56 @@ +# Copyright (c) 2017 Ruben Dulek +# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. + +import re #To perform the search and replace. + +from ..Script import Script + +## Performs a search-and-replace on all g-code. +# +# Due to technical limitations, the search can't cross the border between +# layers. +class SearchAndReplace(Script): + def getSettingDataString(self): + return """{ + "name": "Search and Replace", + "key": "SearchAndReplace", + "metadata": {}, + "version": 2, + "settings": + { + "search": + { + "label": "Search", + "description": "All occurrences of this text will get replaced by the replacement text.", + "type": "str", + "default_value": "" + }, + "replace": + { + "label": "Replace", + "description": "The search text will get replaced by this text.", + "type": "str", + "default_value": "" + }, + "is_regex": + { + "label": "Use Regular Expressions", + "description": "When enabled, the search text will be interpreted as a regular expression.", + "type": "bool", + "default_value": false + } + } + }""" + + def execute(self, data): + search_string = self.getSettingValueByKey("search") + if not self.getSettingValueByKey("is_regex"): + search_string = re.escape(search_string) #Need to search for the actual string, not as a regex. + search_regex = re.compile(search_string) + + replace_string = self.getSettingValueByKey("replace") + + for layer_number, layer in enumerate(data): + data[layer_number] = re.sub(search_regex, replace_string, layer) #Replace all. + + return data \ No newline at end of file diff --git a/plugins/PostProcessingPlugin/scripts/Stretch.py b/plugins/PostProcessingPlugin/scripts/Stretch.py new file mode 100644 index 0000000000..bcb923d3ff --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/Stretch.py @@ -0,0 +1,469 @@ +# This PostProcessingPlugin script is released under the terms of the AGPLv3 or higher. +""" +Copyright (c) 2017 Christophe Baribaud 2017 +Python implementation of https://github.com/electrocbd/post_stretch +Correction of hole sizes, cylinder diameters and curves +See the original description in https://github.com/electrocbd/post_stretch + +WARNING This script has never been tested with several extruders +""" +from ..Script import Script +import numpy as np +from UM.Logger import Logger +from UM.Application import Application +import re + +def _getValue(line, key, default=None): + """ + Convenience function that finds the value in a line of g-code. + When requesting key = x from line "G1 X100" the value 100 is returned. + It is a copy of Stript's method, so it is no DontRepeatYourself, but + I split the class into setup part (Stretch) and execution part (Strecher) + and only the setup part inherits from Script + """ + if not key in line or (";" in line and line.find(key) > line.find(";")): + return default + sub_part = line[line.find(key) + 1:] + number = re.search(r"^-?[0-9]+\.?[0-9]*", sub_part) + if number is None: + return default + return float(number.group(0)) + +class GCodeStep(): + """ + Class to store the current value of each G_Code parameter + for any G-Code step + """ + def __init__(self, step): + self.step = step + self.step_x = 0 + self.step_y = 0 + self.step_z = 0 + self.step_e = 0 + self.step_f = 0 + self.comment = "" + + def readStep(self, line): + """ + Reads gcode from line into self + """ + self.step_x = _getValue(line, "X", self.step_x) + self.step_y = _getValue(line, "Y", self.step_y) + self.step_z = _getValue(line, "Z", self.step_z) + self.step_e = _getValue(line, "E", self.step_e) + self.step_f = _getValue(line, "F", self.step_f) + return + + def copyPosFrom(self, step): + """ + Copies positions of step into self + """ + self.step_x = step.step_x + self.step_y = step.step_y + self.step_z = step.step_z + self.step_e = step.step_e + self.step_f = step.step_f + self.comment = step.comment + return + + +# Execution part of the stretch plugin +class Stretcher(): + """ + Execution part of the stretch algorithm + """ + def __init__(self, line_width, wc_stretch, pw_stretch): + self.line_width = line_width + self.wc_stretch = wc_stretch + self.pw_stretch = pw_stretch + if self.pw_stretch > line_width / 4: + self.pw_stretch = line_width / 4 # Limit value of pushwall stretch distance + self.outpos = GCodeStep(0) + self.vd1 = np.empty((0, 2)) # Start points of segments + # of already deposited material for current layer + self.vd2 = np.empty((0, 2)) # End points of segments + # of already deposited material for current layer + self.layer_z = 0 # Z position of the extrusion moves of the current layer + self.layergcode = "" + + def execute(self, data): + """ + Computes the new X and Y coordinates of all g-code steps + """ + Logger.log("d", "Post stretch with line width = " + str(self.line_width) + + "mm wide circle stretch = " + str(self.wc_stretch)+ "mm" + + "and push wall stretch = " + str(self.pw_stretch) + "mm") + retdata = [] + layer_steps = [] + current = GCodeStep(0) + self.layer_z = 0. + current_e = 0. + for layer in data: + lines = layer.rstrip("\n").split("\n") + for line in lines: + current.comment = "" + if line.find(";") >= 0: + current.comment = line[line.find(";"):] + if _getValue(line, "G") == 0: + current.readStep(line) + onestep = GCodeStep(0) + onestep.copyPosFrom(current) + elif _getValue(line, "G") == 1: + current.readStep(line) + onestep = GCodeStep(1) + onestep.copyPosFrom(current) + elif _getValue(line, "G") == 92: + current.readStep(line) + onestep = GCodeStep(-1) + onestep.copyPosFrom(current) + else: + onestep = GCodeStep(-1) + onestep.copyPosFrom(current) + onestep.comment = line + if line.find(";LAYER:") >= 0 and len(layer_steps): + # Previous plugin "forgot" to separate two layers... + Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z) + + " " + str(len(layer_steps)) + " steps") + retdata.append(self.processLayer(layer_steps)) + layer_steps = [] + layer_steps.append(onestep) + # self.layer_z is the z position of the last extrusion move (not travel move) + if current.step_z != self.layer_z and current.step_e != current_e: + self.layer_z = current.step_z + current_e = current.step_e + if len(layer_steps): # Force a new item in the array + Logger.log("d", "Layer Z " + "{:.3f}".format(self.layer_z) + + " " + str(len(layer_steps)) + " steps") + retdata.append(self.processLayer(layer_steps)) + layer_steps = [] + retdata.append(";Wide circle stretch distance " + str(self.wc_stretch) + "\n") + retdata.append(";Push wall stretch distance " + str(self.pw_stretch) + "\n") + return retdata + + def extrusionBreak(self, layer_steps, i_pos): + """ + Returns true if the command layer_steps[i_pos] breaks the extruded filament + i.e. it is a travel move + """ + if i_pos == 0: + return True # Begining a layer always breaks filament (for simplicity) + step = layer_steps[i_pos] + prev_step = layer_steps[i_pos - 1] + if step.step_e != prev_step.step_e: + return False + delta_x = step.step_x - prev_step.step_x + delta_y = step.step_y - prev_step.step_y + if delta_x * delta_x + delta_y * delta_y < self.line_width * self.line_width / 4: + # This is a very short movement, less than 0.5 * line_width + # It does not break filament, we should stay in the same extrusion sequence + return False + return True # New sequence + + + def processLayer(self, layer_steps): + """ + Computes the new coordinates of g-code steps + for one layer (all the steps at the same Z coordinate) + """ + self.outpos.step_x = -1000 # Force output of X and Y coordinates + self.outpos.step_y = -1000 # at each start of layer + self.layergcode = "" + self.vd1 = np.empty((0, 2)) + self.vd2 = np.empty((0, 2)) + orig_seq = np.empty((0, 2)) + modif_seq = np.empty((0, 2)) + iflush = 0 + for i, step in enumerate(layer_steps): + if step.step == 0 or step.step == 1: + if self.extrusionBreak(layer_steps, i): + # No extrusion since the previous step, so it is a travel move + # Let process steps accumulated into orig_seq, + # which are a sequence of continuous extrusion + modif_seq = np.copy(orig_seq) + if len(orig_seq) >= 2: + self.workOnSequence(orig_seq, modif_seq) + self.generate(layer_steps, iflush, i, modif_seq) + iflush = i + orig_seq = np.empty((0, 2)) + orig_seq = np.concatenate([orig_seq, np.array([[step.step_x, step.step_y]])]) + if len(orig_seq): + modif_seq = np.copy(orig_seq) + if len(orig_seq) >= 2: + self.workOnSequence(orig_seq, modif_seq) + self.generate(layer_steps, iflush, len(layer_steps), modif_seq) + return self.layergcode + + def stepToGcode(self, onestep): + """ + Converts a step into G-Code + For each of the X, Y, Z, E and F parameter, + the parameter is written only if its value changed since the + previous g-code step. + """ + sout = "" + if onestep.step_f != self.outpos.step_f: + self.outpos.step_f = onestep.step_f + sout += " F{:.0f}".format(self.outpos.step_f).rstrip(".") + if onestep.step_x != self.outpos.step_x or onestep.step_y != self.outpos.step_y: + assert onestep.step_x >= -1000 and onestep.step_x < 1000 # If this assertion fails, + # something went really wrong ! + self.outpos.step_x = onestep.step_x + sout += " X{:.3f}".format(self.outpos.step_x).rstrip("0").rstrip(".") + assert onestep.step_y >= -1000 and onestep.step_y < 1000 # If this assertion fails, + # something went really wrong ! + self.outpos.step_y = onestep.step_y + sout += " Y{:.3f}".format(self.outpos.step_y).rstrip("0").rstrip(".") + if onestep.step_z != self.outpos.step_z or onestep.step_z != self.layer_z: + self.outpos.step_z = onestep.step_z + sout += " Z{:.3f}".format(self.outpos.step_z).rstrip("0").rstrip(".") + if onestep.step_e != self.outpos.step_e: + self.outpos.step_e = onestep.step_e + sout += " E{:.5f}".format(self.outpos.step_e).rstrip("0").rstrip(".") + return sout + + def generate(self, layer_steps, ibeg, iend, orig_seq): + """ + Appends g-code lines to the plugin's returned string + starting from step ibeg included and until step iend excluded + """ + ipos = 0 + for i in range(ibeg, iend): + if layer_steps[i].step == 0: + layer_steps[i].step_x = orig_seq[ipos][0] + layer_steps[i].step_y = orig_seq[ipos][1] + sout = "G0" + self.stepToGcode(layer_steps[i]) + self.layergcode = self.layergcode + sout + "\n" + ipos = ipos + 1 + elif layer_steps[i].step == 1: + layer_steps[i].step_x = orig_seq[ipos][0] + layer_steps[i].step_y = orig_seq[ipos][1] + sout = "G1" + self.stepToGcode(layer_steps[i]) + self.layergcode = self.layergcode + sout + "\n" + ipos = ipos + 1 + else: + self.layergcode = self.layergcode + layer_steps[i].comment + "\n" + + + def workOnSequence(self, orig_seq, modif_seq): + """ + Computes new coordinates for a sequence + A sequence is a list of consecutive g-code steps + of continuous material extrusion + """ + d_contact = self.line_width / 2.0 + if (len(orig_seq) > 2 and + ((orig_seq[len(orig_seq) - 1] - orig_seq[0]) ** 2).sum(0) < d_contact * d_contact): + # Starting and ending point of the sequence are nearby + # It is a closed loop + #self.layergcode = self.layergcode + ";wideCircle\n" + self.wideCircle(orig_seq, modif_seq) + else: + #self.layergcode = self.layergcode + ";wideTurn\n" + self.wideTurn(orig_seq, modif_seq) # It is an open curve + if len(orig_seq) > 6: # Don't try push wall on a short sequence + self.pushWall(orig_seq, modif_seq) + if len(orig_seq): + self.vd1 = np.concatenate([self.vd1, np.array(orig_seq[:-1])]) + self.vd2 = np.concatenate([self.vd2, np.array(orig_seq[1:])]) + + def wideCircle(self, orig_seq, modif_seq): + """ + Similar to wideTurn + The first and last point of the sequence are the same, + so it is possible to extend the end of the sequence + with its beginning when seeking for triangles + + It is necessary to find the direction of the curve, knowing three points (a triangle) + If the triangle is not wide enough, there is a huge risk of finding + an incorrect orientation, due to insufficient accuracy. + So, when the consecutive points are too close, the method + use following and preceding points to form a wider triangle around + the current point + dmin_tri is the minimum distance between two consecutive points + of an acceptable triangle + """ + dmin_tri = self.line_width / 2.0 + iextra_base = np.floor_divide(len(orig_seq), 3) # Nb of extra points + ibeg = 0 # Index of first point of the triangle + iend = 0 # Index of the third point of the triangle + for i, step in enumerate(orig_seq): + if i == 0 or i == len(orig_seq) - 1: + # First and last point of the sequence are the same, + # so it is necessary to skip one of these two points + # when creating a triangle containing the first or the last point + iextra = iextra_base + 1 + else: + iextra = iextra_base + # i is the index of the second point of the triangle + # pos_after is the array of positions of the original sequence + # after the current point + pos_after = np.resize(np.roll(orig_seq, -i-1, 0), (iextra, 2)) + # Vector of distances between the current point and each following point + dist_from_point = ((step - pos_after) ** 2).sum(1) + if np.amax(dist_from_point) < dmin_tri * dmin_tri: + continue + iend = np.argmax(dist_from_point >= dmin_tri * dmin_tri) + # pos_before is the array of positions of the original sequence + # before the current point + pos_before = np.resize(np.roll(orig_seq, -i, 0)[::-1], (iextra, 2)) + # This time, vector of distances between the current point and each preceding point + dist_from_point = ((step - pos_before) ** 2).sum(1) + if np.amax(dist_from_point) < dmin_tri * dmin_tri: + continue + ibeg = np.argmax(dist_from_point >= dmin_tri * dmin_tri) + # See https://github.com/electrocbd/post_stretch for explanations + # relpos is the relative position of the projection of the second point + # of the triangle on the segment from the first to the third point + # 0 means the position of the first point, 1 means the position of the third, + # intermediate values are positions between + length_base = ((pos_after[iend] - pos_before[ibeg]) ** 2).sum(0) + relpos = ((step - pos_before[ibeg]) + * (pos_after[iend] - pos_before[ibeg])).sum(0) + if np.fabs(relpos) < 1000.0 * np.fabs(length_base): + relpos /= length_base + else: + relpos = 0.5 # To avoid division by zero or precision loss + projection = (pos_before[ibeg] + relpos * (pos_after[iend] - pos_before[ibeg])) + dist_from_proj = np.sqrt(((projection - step) ** 2).sum(0)) + if dist_from_proj > 0.001: # Move central point only if points are not aligned + modif_seq[i] = (step - (self.wc_stretch / dist_from_proj) + * (projection - step)) + return + + def wideTurn(self, orig_seq, modif_seq): + ''' + We have to select three points in order to form a triangle + These three points should be far enough from each other to have + a reliable estimation of the orientation of the current turn + ''' + dmin_tri = self.line_width / 2.0 + ibeg = 0 + iend = 2 + for i in range(1, len(orig_seq) - 1): + dist_from_point = ((orig_seq[i] - orig_seq[i+1:]) ** 2).sum(1) + if np.amax(dist_from_point) < dmin_tri * dmin_tri: + continue + iend = i + 1 + np.argmax(dist_from_point >= dmin_tri * dmin_tri) + dist_from_point = ((orig_seq[i] - orig_seq[i-1::-1]) ** 2).sum(1) + if np.amax(dist_from_point) < dmin_tri * dmin_tri: + continue + ibeg = i - 1 - np.argmax(dist_from_point >= dmin_tri * dmin_tri) + length_base = ((orig_seq[iend] - orig_seq[ibeg]) ** 2).sum(0) + relpos = ((orig_seq[i] - orig_seq[ibeg]) * (orig_seq[iend] - orig_seq[ibeg])).sum(0) + if np.fabs(relpos) < 1000.0 * np.fabs(length_base): + relpos /= length_base + else: + relpos = 0.5 + projection = orig_seq[ibeg] + relpos * (orig_seq[iend] - orig_seq[ibeg]) + dist_from_proj = np.sqrt(((projection - orig_seq[i]) ** 2).sum(0)) + if dist_from_proj > 0.001: + modif_seq[i] = (orig_seq[i] - (self.wc_stretch / dist_from_proj) + * (projection - orig_seq[i])) + return + + def pushWall(self, orig_seq, modif_seq): + """ + The algorithm tests for each segment if material was + already deposited at one or the other side of this segment. + If material was deposited at one side but not both, + the segment is moved into the direction of the deposited material, + to "push the wall" + + Already deposited material is stored as segments. + vd1 is the array of the starting points of the segments + vd2 is the array of the ending points of the segments + For example, segment nr 8 starts at position self.vd1[8] + and ends at position self.vd2[8] + """ + dist_palp = self.line_width # Palpation distance to seek for a wall + mrot = np.array([[0, -1], [1, 0]]) # Rotation matrix for a quarter turn + for i in range(len(orig_seq)): + ibeg = i # Index of the first point of the segment + iend = i + 1 # Index of the last point of the segment + if iend == len(orig_seq): + iend = i - 1 + xperp = np.dot(mrot, orig_seq[iend] - orig_seq[ibeg]) + xperp = xperp / np.sqrt((xperp ** 2).sum(-1)) + testleft = orig_seq[ibeg] + xperp * dist_palp + materialleft = False # Is there already extruded material at the left of the segment + testright = orig_seq[ibeg] - xperp * dist_palp + materialright = False # Is there already extruded material at the right of the segment + if self.vd1.shape[0]: + relpos = np.clip(((testleft - self.vd1) * (self.vd2 - self.vd1)).sum(1) + / ((self.vd2 - self.vd1) * (self.vd2 - self.vd1)).sum(1), 0., 1.) + nearpoints = self.vd1 + relpos[:, np.newaxis] * (self.vd2 - self.vd1) + # nearpoints is the array of the nearest points of each segment + # from the point testleft + dist = ((testleft - nearpoints) * (testleft - nearpoints)).sum(1) + # dist is the array of the squares of the distances between testleft + # and each segment + if np.amin(dist) <= dist_palp * dist_palp: + materialleft = True + # Now the same computation with the point testright at the other side of the + # current segment + relpos = np.clip(((testright - self.vd1) * (self.vd2 - self.vd1)).sum(1) + / ((self.vd2 - self.vd1) * (self.vd2 - self.vd1)).sum(1), 0., 1.) + nearpoints = self.vd1 + relpos[:, np.newaxis] * (self.vd2 - self.vd1) + dist = ((testright - nearpoints) * (testright - nearpoints)).sum(1) + if np.amin(dist) <= dist_palp * dist_palp: + materialright = True + if materialleft and not materialright: + modif_seq[ibeg] = modif_seq[ibeg] + xperp * self.pw_stretch + elif not materialleft and materialright: + modif_seq[ibeg] = modif_seq[ibeg] - xperp * self.pw_stretch + if materialleft and materialright: + modif_seq[ibeg] = orig_seq[ibeg] # Surrounded by walls, don't move + +# Setup part of the stretch plugin +class Stretch(Script): + """ + Setup part of the stretch algorithm + The only parameter is the stretch distance + """ + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"Post stretch script", + "key": "Stretch", + "metadata": {}, + "version": 2, + "settings": + { + "wc_stretch": + { + "label": "Wide circle stretch distance", + "description": "Distance by which the points are moved by the correction effect in corners. The higher this value, the higher the effect", + "unit": "mm", + "type": "float", + "default_value": 0.08, + "minimum_value": 0, + "minimum_value_warning": 0, + "maximum_value_warning": 0.2 + }, + "pw_stretch": + { + "label": "Push Wall stretch distance", + "description": "Distance by which the points are moved by the correction effect when two lines are nearby. The higher this value, the higher the effect", + "unit": "mm", + "type": "float", + "default_value": 0.08, + "minimum_value": 0, + "minimum_value_warning": 0, + "maximum_value_warning": 0.2 + } + } + }""" + + def execute(self, data): + """ + Entry point of the plugin. + data is the list of original g-code instructions, + the returned string is the list of modified g-code instructions + """ + stretcher = Stretcher( + Application.getInstance().getGlobalContainerStack().getProperty("line_width", "value") + , self.getSettingValueByKey("wc_stretch"), self.getSettingValueByKey("pw_stretch")) + return stretcher.execute(data) + diff --git a/plugins/PostProcessingPlugin/scripts/TweakAtZ.py b/plugins/PostProcessingPlugin/scripts/TweakAtZ.py new file mode 100644 index 0000000000..7b714f6ee0 --- /dev/null +++ b/plugins/PostProcessingPlugin/scripts/TweakAtZ.py @@ -0,0 +1,495 @@ +# TweakAtZ script - Change printing parameters at a given height +# This script is the successor of the TweakAtZ plugin for legacy Cura. +# It contains code from the TweakAtZ plugin V1.0-V4.x and from the ExampleScript by Jaime van Kessel, Ultimaker B.V. +# It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher. +# This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms + +#Authors of the TweakAtZ plugin / script: +# Written by Steven Morlock, smorloc@gmail.com +# Modified by Ricardo Gomez, ricardoga@otulook.com, to add Bed Temperature and make it work with Cura_13.06.04+ +# Modified by Stefan Heule, Dim3nsioneer@gmx.ch since V3.0 (see changelog below) +# Modified by Jaime van Kessel (Ultimaker), j.vankessel@ultimaker.com to make it work for 15.10 / 2.x +# Modified by Ruben Dulek (Ultimaker), r.dulek@ultimaker.com, to debug. + +##history / changelog: +##V3.0.1: TweakAtZ-state default 1 (i.e. the plugin works without any TweakAtZ comment) +##V3.1: Recognizes UltiGCode and deactivates value reset, fan speed added, alternatively layer no. to tweak at, +## extruder three temperature disabled by "#Ex3" +##V3.1.1: Bugfix reset flow rate +##V3.1.2: Bugfix disable TweakAtZ on Cool Head Lift +##V3.2: Flow rate for specific extruder added (only for 2 extruders), bugfix parser, +## added speed reset at the end of the print +##V4.0: Progress bar, tweaking over multiple layers, M605&M606 implemented, reset after one layer option, +## extruder three code removed, tweaking print speed, save call of Publisher class, +## uses previous value from other plugins also on UltiGCode +##V4.0.1: Bugfix for doubled G1 commands +##V4.0.2: uses Cura progress bar instead of its own +##V4.0.3: Bugfix for cool head lift (contributed by luisonoff) +##V4.9.91: First version for Cura 15.06.x and PostProcessingPlugin +##V4.9.92: Modifications for Cura 15.10 +##V4.9.93: Minor bugfixes (input settings) / documentation +##V4.9.94: Bugfix Combobox-selection; remove logger +##V5.0: Bugfix for fall back after one layer and doubled G0 commands when using print speed tweak, Initial version for Cura 2.x +##V5.0.1: Bugfix for calling unknown property 'bedTemp' of previous settings storage and unkown variable 'speed' +##V5.1: API Changes included for use with Cura 2.2 + +## Uses - +## M220 S - set speed factor override percentage +## M221 S - set flow factor override percentage +## M221 S T<0-#toolheads> - set flow factor override percentage for single extruder +## M104 S T<0-#toolheads> - set extruder to target temperature +## M140 S - set bed target temperature +## M106 S - set fan speed to target speed +## M605/606 to save and recall material settings on the UM2 + +from ..Script import Script +#from UM.Logger import Logger +import re + +class TweakAtZ(Script): + version = "5.1.1" + def __init__(self): + super().__init__() + + def getSettingDataString(self): + return """{ + "name":"TweakAtZ """ + self.version + """ (Experimental)", + "key":"TweakAtZ", + "metadata": {}, + "version": 2, + "settings": + { + "a_trigger": + { + "label": "Trigger", + "description": "Trigger at height or at layer no.", + "type": "enum", + "options": {"height":"Height","layer_no":"Layer No."}, + "default_value": "height" + }, + "b_targetZ": + { + "label": "Tweak Height", + "description": "Z height to tweak at", + "unit": "mm", + "type": "float", + "default_value": 5.0, + "minimum_value": "0", + "minimum_value_warning": "0.1", + "maximum_value_warning": "230", + "enabled": "a_trigger == 'height'" + }, + "b_targetL": + { + "label": "Tweak Layer", + "description": "Layer no. to tweak at", + "unit": "", + "type": "int", + "default_value": 1, + "minimum_value": "-100", + "minimum_value_warning": "-1", + "enabled": "a_trigger == 'layer_no'" + }, + "c_behavior": + { + "label": "Behavior", + "description": "Select behavior: Tweak value and keep it for the rest, Tweak value for single layer only", + "type": "enum", + "options": {"keep_value":"Keep value","single_layer":"Single Layer"}, + "default_value": "keep_value" + }, + "d_twLayers": + { + "label": "No. Layers", + "description": "No. of layers used to tweak", + "unit": "", + "type": "int", + "default_value": 1, + "minimum_value": "1", + "maximum_value_warning": "50", + "enabled": "c_behavior == 'keep_value'" + }, + "e1_Tweak_speed": + { + "label": "Tweak Speed", + "description": "Select if total speed (print and travel) has to be tweaked", + "type": "bool", + "default_value": false + }, + "e2_speed": + { + "label": "Speed", + "description": "New total speed (print and travel)", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "e1_Tweak_speed" + }, + "f1_Tweak_printspeed": + { + "label": "Tweak Print Speed", + "description": "Select if print speed has to be tweaked", + "type": "bool", + "default_value": false + }, + "f2_printspeed": + { + "label": "Print Speed", + "description": "New print speed", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "f1_Tweak_printspeed" + }, + "g1_Tweak_flowrate": + { + "label": "Tweak Flow Rate", + "description": "Select if flow rate has to be tweaked", + "type": "bool", + "default_value": false + }, + "g2_flowrate": + { + "label": "Flow Rate", + "description": "New Flow rate", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "g1_Tweak_flowrate" + }, + "g3_Tweak_flowrateOne": + { + "label": "Tweak Flow Rate 1", + "description": "Select if first extruder flow rate has to be tweaked", + "type": "bool", + "default_value": false + }, + "g4_flowrateOne": + { + "label": "Flow Rate One", + "description": "New Flow rate Extruder 1", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "g3_Tweak_flowrateOne" + }, + "g5_Tweak_flowrateTwo": + { + "label": "Tweak Flow Rate 2", + "description": "Select if second extruder flow rate has to be tweaked", + "type": "bool", + "default_value": false + }, + "g6_flowrateTwo": + { + "label": "Flow Rate two", + "description": "New Flow rate Extruder 2", + "unit": "%", + "type": "int", + "default_value": 100, + "minimum_value": "1", + "minimum_value_warning": "10", + "maximum_value_warning": "200", + "enabled": "g5_Tweak_flowrateTwo" + }, + "h1_Tweak_bedTemp": + { + "label": "Tweak Bed Temp", + "description": "Select if Bed Temperature has to be tweaked", + "type": "bool", + "default_value": false + }, + "h2_bedTemp": + { + "label": "Bed Temp", + "description": "New Bed Temperature", + "unit": "C", + "type": "float", + "default_value": 60, + "minimum_value": "0", + "minimum_value_warning": "30", + "maximum_value_warning": "120", + "enabled": "h1_Tweak_bedTemp" + }, + "i1_Tweak_extruderOne": + { + "label": "Tweak Extruder 1 Temp", + "description": "Select if First Extruder Temperature has to be tweaked", + "type": "bool", + "default_value": false + }, + "i2_extruderOne": + { + "label": "Extruder 1 Temp", + "description": "New First Extruder Temperature", + "unit": "C", + "type": "float", + "default_value": 190, + "minimum_value": "0", + "minimum_value_warning": "160", + "maximum_value_warning": "250", + "enabled": "i1_Tweak_extruderOne" + }, + "i3_Tweak_extruderTwo": + { + "label": "Tweak Extruder 2 Temp", + "description": "Select if Second Extruder Temperature has to be tweaked", + "type": "bool", + "default_value": false + }, + "i4_extruderTwo": + { + "label": "Extruder 2 Temp", + "description": "New Second Extruder Temperature", + "unit": "C", + "type": "float", + "default_value": 190, + "minimum_value": "0", + "minimum_value_warning": "160", + "maximum_value_warning": "250", + "enabled": "i3_Tweak_extruderTwo" + }, + "j1_Tweak_fanSpeed": + { + "label": "Tweak Fan Speed", + "description": "Select if Fan Speed has to be tweaked", + "type": "bool", + "default_value": false + }, + "j2_fanSpeed": + { + "label": "Fan Speed", + "description": "New Fan Speed (0-255)", + "unit": "PWM", + "type": "int", + "default_value": 255, + "minimum_value": "0", + "minimum_value_warning": "15", + "maximum_value_warning": "255", + "enabled": "j1_Tweak_fanSpeed" + } + } + }""" + + def getValue(self, line, key, default = None): #replace default getvalue due to comment-reading feature + if not key in line or (";" in line and line.find(key) > line.find(";") and + not ";TweakAtZ" in key and not ";LAYER:" in key): + return default + subPart = line[line.find(key) + len(key):] #allows for string lengths larger than 1 + if ";TweakAtZ" in key: + m = re.search("^[0-4]", subPart) + elif ";LAYER:" in key: + m = re.search("^[+-]?[0-9]*", subPart) + else: + #the minus at the beginning allows for negative values, e.g. for delta printers + m = re.search("^[-]?[0-9]*\.?[0-9]*", subPart) + if m == None: + return default + try: + return float(m.group(0)) + except: + return default + + def execute(self, data): + #Check which tweaks should apply + TweakProp = {"speed": self.getSettingValueByKey("e1_Tweak_speed"), + "flowrate": self.getSettingValueByKey("g1_Tweak_flowrate"), + "flowrateOne": self.getSettingValueByKey("g3_Tweak_flowrateOne"), + "flowrateTwo": self.getSettingValueByKey("g5_Tweak_flowrateTwo"), + "bedTemp": self.getSettingValueByKey("h1_Tweak_bedTemp"), + "extruderOne": self.getSettingValueByKey("i1_Tweak_extruderOne"), + "extruderTwo": self.getSettingValueByKey("i3_Tweak_extruderTwo"), + "fanSpeed": self.getSettingValueByKey("j1_Tweak_fanSpeed")} + TweakPrintSpeed = self.getSettingValueByKey("f1_Tweak_printspeed") + TweakStrings = {"speed": "M220 S%f\n", + "flowrate": "M221 S%f\n", + "flowrateOne": "M221 T0 S%f\n", + "flowrateTwo": "M221 T1 S%f\n", + "bedTemp": "M140 S%f\n", + "extruderOne": "M104 S%f T0\n", + "extruderTwo": "M104 S%f T1\n", + "fanSpeed": "M106 S%d\n"} + target_values = {"speed": self.getSettingValueByKey("e2_speed"), + "printspeed": self.getSettingValueByKey("f2_printspeed"), + "flowrate": self.getSettingValueByKey("g2_flowrate"), + "flowrateOne": self.getSettingValueByKey("g4_flowrateOne"), + "flowrateTwo": self.getSettingValueByKey("g6_flowrateTwo"), + "bedTemp": self.getSettingValueByKey("h2_bedTemp"), + "extruderOne": self.getSettingValueByKey("i2_extruderOne"), + "extruderTwo": self.getSettingValueByKey("i4_extruderTwo"), + "fanSpeed": self.getSettingValueByKey("j2_fanSpeed")} + old = {"speed": -1, "flowrate": -1, "flowrateOne": -1, "flowrateTwo": -1, "platformTemp": -1, "extruderOne": -1, + "extruderTwo": -1, "bedTemp": -1, "fanSpeed": -1, "state": -1} + twLayers = self.getSettingValueByKey("d_twLayers") + if self.getSettingValueByKey("c_behavior") == "single_layer": + behavior = 1 + else: + behavior = 0 + try: + twLayers = max(int(twLayers),1) #for the case someone entered something as "funny" as -1 + except: + twLayers = 1 + pres_ext = 0 + done_layers = 0 + z = 0 + x = None + y = None + layer = -100000 #layer no. may be negative (raft) but never that low + # state 0: deactivated, state 1: activated, state 2: active, but below z, + # state 3: active and partially executed (multi layer), state 4: active and passed z + state = 1 + # IsUM2: Used for reset of values (ok for Marlin/Sprinter), + # has to be set to 1 for UltiGCode (work-around for missing default values) + IsUM2 = False + oldValueUnknown = False + TWinstances = 0 + + if self.getSettingValueByKey("a_trigger") == "layer_no": + targetL_i = int(self.getSettingValueByKey("b_targetL")) + targetZ = 100000 + else: + targetL_i = -100000 + targetZ = self.getSettingValueByKey("b_targetZ") + index = 0 + for active_layer in data: + modified_gcode = "" + lines = active_layer.split("\n") + for line in lines: + if ";Generated with Cura_SteamEngine" in line: + TWinstances += 1 + modified_gcode += ";TweakAtZ instances: %d\n" % TWinstances + if not ("M84" in line or "M25" in line or ("G1" in line and TweakPrintSpeed and (state==3 or state==4)) or + ";TweakAtZ instances:" in line): + modified_gcode += line + "\n" + IsUM2 = ("FLAVOR:UltiGCode" in line) or IsUM2 #Flavor is UltiGCode! + if ";TweakAtZ-state" in line: #checks for state change comment + state = self.getValue(line, ";TweakAtZ-state", state) + if ";TweakAtZ instances:" in line: + try: + tempTWi = int(line[20:]) + except: + tempTWi = TWinstances + TWinstances = tempTWi + if ";Small layer" in line: #checks for begin of Cool Head Lift + old["state"] = state + state = 0 + if ";LAYER:" in line: #new layer no. found + if state == 0: + state = old["state"] + layer = self.getValue(line, ";LAYER:", layer) + if targetL_i > -100000: #target selected by layer no. + if (state == 2 or targetL_i == 0) and layer == targetL_i: #determine targetZ from layer no.; checks for tweak on layer 0 + state = 2 + targetZ = z + 0.001 + if (self.getValue(line, "T", None) is not None) and (self.getValue(line, "M", None) is None): #looking for single T-cmd + pres_ext = self.getValue(line, "T", pres_ext) + if "M190" in line or "M140" in line and state < 3: #looking for bed temp, stops after target z is passed + old["bedTemp"] = self.getValue(line, "S", old["bedTemp"]) + if "M109" in line or "M104" in line and state < 3: #looking for extruder temp, stops after target z is passed + if self.getValue(line, "T", pres_ext) == 0: + old["extruderOne"] = self.getValue(line, "S", old["extruderOne"]) + elif self.getValue(line, "T", pres_ext) == 1: + old["extruderTwo"] = self.getValue(line, "S", old["extruderTwo"]) + if "M107" in line: #fan is stopped; is always updated in order not to miss switch off for next object + old["fanSpeed"] = 0 + if "M106" in line and state < 3: #looking for fan speed + old["fanSpeed"] = self.getValue(line, "S", old["fanSpeed"]) + if "M221" in line and state < 3: #looking for flow rate + tmp_extruder = self.getValue(line,"T",None) + if tmp_extruder == None: #check if extruder is specified + old["flowrate"] = self.getValue(line, "S", old["flowrate"]) + elif tmp_extruder == 0: #first extruder + old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"]) + elif tmp_extruder == 1: #second extruder + old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"]) + if ("M84" in line or "M25" in line): + if state>0 and TweakProp["speed"]: #"finish" commands for UM Original and UM2 + modified_gcode += "M220 S100 ; speed reset to 100% at the end of print\n" + modified_gcode += "M117 \n" + modified_gcode += line + "\n" + if "G1" in line or "G0" in line: + newZ = self.getValue(line, "Z", z) + x = self.getValue(line, "X", None) + y = self.getValue(line, "Y", None) + e = self.getValue(line, "E", None) + f = self.getValue(line, "F", None) + if 'G1' in line and TweakPrintSpeed and (state==3 or state==4): + # check for pure print movement in target range: + if x != None and y != None and f != None and e != None and newZ==z: + modified_gcode += "G1 F%d X%1.3f Y%1.3f E%1.5f\n" % (int(f / 100.0 * float(target_values["printspeed"])), self.getValue(line, "X"), + self.getValue(line, "Y"), self.getValue(line, "E")) + else: #G1 command but not a print movement + modified_gcode += line + "\n" + # no tweaking on retraction hops which have no x and y coordinate: + if (newZ != z) and (x is not None) and (y is not None): + z = newZ + if z < targetZ and state == 1: + state = 2 + if z >= targetZ and state == 2: + state = 3 + done_layers = 0 + for key in TweakProp: + if TweakProp[key] and old[key]==-1: #old value is not known + oldValueUnknown = True + if oldValueUnknown: #the tweaking has to happen within one layer + twLayers = 1 + if IsUM2: #Parameters have to be stored in the printer (UltiGCode=UM2) + modified_gcode += "M605 S%d;stores parameters before tweaking\n" % (TWinstances-1) + if behavior == 1: #single layer tweak only and then reset + twLayers = 1 + if TweakPrintSpeed and behavior == 0: + twLayers = done_layers + 1 + if state==3: + if twLayers-done_layers>0: #still layers to go? + if targetL_i > -100000: + modified_gcode += ";TweakAtZ V%s: executed at Layer %d\n" % (self.version,layer) + modified_gcode += "M117 Printing... tw@L%4d\n" % layer + else: + modified_gcode += (";TweakAtZ V%s: executed at %1.2f mm\n" % (self.version,z)) + modified_gcode += "M117 Printing... tw@%5.1f\n" % z + for key in TweakProp: + if TweakProp[key]: + modified_gcode += TweakStrings[key] % float(old[key]+(float(target_values[key])-float(old[key]))/float(twLayers)*float(done_layers+1)) + done_layers += 1 + else: + state = 4 + if behavior == 1: #reset values after one layer + if targetL_i > -100000: + modified_gcode += ";TweakAtZ V%s: reset on Layer %d\n" % (self.version,layer) + else: + modified_gcode += ";TweakAtZ V%s: reset at %1.2f mm\n" % (self.version,z) + if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting + modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1) + else: #executes on RepRap, UM2 with Ultigcode and Cura setting + for key in TweakProp: + if TweakProp[key]: + modified_gcode += TweakStrings[key] % float(old[key]) + # re-activates the plugin if executed by pre-print G-command, resets settings: + if (z < targetZ or layer == 0) and state >= 3: #resets if below tweak level or at level 0 + state = 2 + done_layers = 0 + if targetL_i > -100000: + modified_gcode += ";TweakAtZ V%s: reset below Layer %d\n" % (self.version,targetL_i) + else: + modified_gcode += ";TweakAtZ V%s: reset below %1.2f mm\n" % (self.version,targetZ) + if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting + modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1) + else: #executes on RepRap, UM2 with Ultigcode and Cura setting + for key in TweakProp: + if TweakProp[key]: + modified_gcode += TweakStrings[key] % float(old[key]) + data[index] = modified_gcode + index += 1 + return data