Add post processing source files

This commit is contained in:
ChrisTerBeke 2018-01-09 10:31:12 +01:00
parent 9c9c46aade
commit 6c0fb110fe
15 changed files with 2463 additions and 0 deletions

View file

@ -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")

View file

@ -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{ }
}
}
}

View file

@ -0,0 +1,2 @@
# PostProcessingPlugin
A post processing plugin for Cura

View file

@ -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()

View file

@ -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()}

View file

@ -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"
}

View file

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
width="512px"
height="512px"
viewBox="0 0 512 512"
style="enable-background:new 0 0 512 512;"
xml:space="preserve"
inkscape:version="0.91 r13725"
sodipodi:docname="postprocessing.svg"><metadata
id="metadata9"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs7" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1104"
inkscape:window-height="1006"
id="namedview5"
showgrid="false"
inkscape:zoom="1.3359375"
inkscape:cx="256"
inkscape:cy="256"
inkscape:window-x="701"
inkscape:window-y="121"
inkscape:window-maximized="0"
inkscape:current-layer="Layer_1" /><path
d="M 402.15234 0 C 371.74552 4.7369516e-015 345.79114 10.752017 324.21875 32.324219 C 302.57788 53.89652 291.82617 79.851497 291.82617 110.18945 C 291.82617 127.34315 295.26662 143.09419 302.16602 157.44531 L 238.38477 221.20312 C 227.77569 210.95036 218.04331 201.50935 209.66016 193.32422 C 207.33386 190.99792 202.68042 189.48707 198.60938 191.92969 L 191.74609 196.11719 C 165.34252 169.24836 154.17609 158.42965 150.57031 145.40234 C 146.84822 131.79345 150.22148 113.64862 153.71094 106.90234 C 156.61882 101.55183 165.69233 96.550326 173.36914 95.96875 L 183.37109 106.20508 C 185.69739 108.53139 189.30456 108.53139 191.63086 106.20508 L 227.57227 69.681641 C 229.89858 67.355335 229.89858 63.517712 227.57227 61.191406 L 169.53125 2.21875 C 167.20494 -0.10755598 163.48147 -0.10755598 161.27148 2.21875 L 125.33008 38.742188 C 123.00378 41.068494 123.00378 44.906116 125.33008 47.232422 L 129.16992 51.1875 C 129.16992 56.88695 128.35573 65.727167 123.70312 70.496094 C 116.49157 77.823958 102.18413 69.332919 92.878906 75.962891 C 83.689998 82.476548 72.05746 92.944493 64.613281 100.38867 C 57.285417 107.83285 29.138171 137.37722 9.015625 187.16016 C -11.106922 236.94311 4.3632369 283.12 15.296875 295.2168 C 21.11264 301.61414 31.696982 308.12804 29.835938 296.03125 C 27.974892 283.81815 24.951448 241.47942 38.792969 224.14844 C 52.634489 206.81746 70.894726 192.62799 94.623047 191.46484 C 117.42084 190.30169 130.56529 198.09417 160.10938 228.10352 L 156.85156 234.15234 C 154.75788 238.10706 155.92175 243.10728 158.24805 245.43359 C 161.95717 248.74082 172.37305 258.96006 186.52539 273.04297 L 6.9511719 452.54883 C 2.2984329 457.14417 1.1842379e-015 462.71497 0 469.14844 C -1.1842379e-015 475.69681 2.2984329 481.15473 6.9511719 485.51953 L 26.308594 505.22266 C 31.018838 509.76054 36.589603 512 42.908203 512 C 49.341623 512 54.800053 509.76054 59.337891 505.22266 L 238.96875 325.6582 C 317.6609 404.95524 424.21289 513.40234 424.21289 513.40234 L 482.25391 454.43164 C 437.71428 411.9686 358.71135 336.76293 291.93164 272.71484 L 354.68945 209.98047 C 369.08663 216.91399 384.90203 220.37891 402.15234 220.37891 C 425.29988 220.37891 446.52947 213.53073 465.77344 199.83398 C 485.08493 186.1372 498.57775 168.33291 506.31641 146.34961 C 510.08303 135.39222 512 126.69334 512 120.25586 C 512 117.79044 511.24662 115.80572 509.87695 114.16211 C 508.50726 112.5185 506.59041 111.69531 504.125 111.69531 C 502.61835 111.69531 496.86414 114.5734 486.72852 120.39453 C 476.6614 126.21564 465.50054 132.85752 453.37891 140.32227 C 441.18878 147.78698 434.7515 151.75888 433.92969 152.23828 L 386.40234 125.94141 L 386.40234 70.8125 L 458.51562 29.242188 C 461.18649 27.461587 462.48633 25.202356 462.48633 22.394531 C 462.48633 19.586706 461.1865 17.325625 458.51562 15.476562 C 451.32484 10.545729 442.4896 6.780346 432.08008 4.0410156 C 421.60206 1.3701797 411.67159 0 402.15234 0 z "
id="path3" /></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

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

View file

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

View file

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

View file

@ -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: <current_height> = <current_z> - <layer_0_z>
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

View file

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

View file

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

View file

@ -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)

View file

@ -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<factor in percent> - set speed factor override percentage
## M221 S<factor in percent> - set flow factor override percentage
## M221 S<factor in percent> T<0-#toolheads> - set flow factor override percentage for single extruder
## M104 S<temp> T<0-#toolheads> - set extruder <T> to target temperature <S>
## M140 S<temp> - set bed target temperature
## M106 S<PWM> - set fan speed to target speed <S>
## 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