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 @@
+
+
+
+
\ 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