Merge branch 'master' into feature_xmlmaterials_cura_settings

# Conflicts:
#	plugins/XmlMaterialProfile/XmlMaterialProfile.py
This commit is contained in:
fieldOfView 2018-03-06 11:40:58 +01:00
commit 0578e74e71
840 changed files with 56973 additions and 26870 deletions

View file

@ -1,29 +1,31 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
import zipfile
from UM.Job import Job
import numpy
import Savitar
from UM.Application import Application
from UM.Logger import Logger
from UM.Math.Matrix import Matrix
from UM.Math.Vector import Vector
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Mesh.MeshReader import MeshReader
from UM.Scene.GroupDecorator import GroupDecorator
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
from UM.Application import Application
from cura.Settings.ExtruderManager import ExtruderManager
from cura.QualityManager import QualityManager
from UM.Scene.SceneNode import SceneNode
from cura.SliceableObjectDecorator import SliceableObjectDecorator
from cura.ZOffsetDecorator import ZOffsetDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
MYPY = False
import Savitar
import numpy
try:
if not MYPY:
import xml.etree.cElementTree as ET
@ -37,12 +39,9 @@ class ThreeMFReader(MeshReader):
super().__init__()
self._supported_extensions = [".3mf"]
self._root = None
self._namespaces = {
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
}
self._base_name = ""
self._unit = None
self._object_count = 0 # Used to name objects as there is no node name yet.
def _createMatrixFromTransformationString(self, transformation):
if transformation == "":
@ -77,7 +76,14 @@ class ThreeMFReader(MeshReader):
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium scene node.
# \returns Uranium scene node.
def _convertSavitarNodeToUMNode(self, savitar_node):
um_node = SceneNode()
self._object_count += 1
node_name = "Object %s" % self._object_count
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
um_node = CuraSceneNode()
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
um_node.setName(node_name)
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
um_node.setTransformation(transformation)
mesh_builder = MeshBuilder()
@ -116,8 +122,8 @@ class ThreeMFReader(MeshReader):
um_node.callDecoration("setActiveExtruder", default_stack.getId())
# Get the definition & set it
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
um_node.callDecoration("getStack").getTop().setDefinition(definition.getId())
definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack)
um_node.callDecoration("getStack").getTop().setDefinition(definition_id)
setting_container = um_node.callDecoration("getStack").getTop()
@ -147,6 +153,7 @@ class ThreeMFReader(MeshReader):
def read(self, file_name):
result = []
self._object_count = 0 # Used to name objects as there is no node name yet.
# The base object of 3mf is a zipped archive.
try:
archive = zipfile.ZipFile(file_name, "r")

View file

@ -23,17 +23,49 @@ from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.CuraContainerStack import _ContainerIndexes
from cura.QualityManager import QualityManager
from cura.CuraApplication import CuraApplication
from configparser import ConfigParser
import zipfile
import io
import configparser
import os
import threading
i18n_catalog = i18nCatalog("cura")
#
# HACK:
#
# In project loading, when override the existing machine is selected, the stacks and containers that are correctly
# active in the system will be overridden at runtime. Because the project loading is done in a different thread than
# the Qt thread, something else can kick in the middle of the process. One of them is the rendering. It will access
# the current stacks and container, which have not completely been updated yet, so Cura will crash in this case.
#
# This "@call_on_qt_thread" decorator makes sure that a function will always be called on the Qt thread (blocking).
# It is applied to the read() function of project loading so it can be guaranteed that only after the project loading
# process is completely done, everything else that needs to occupy the QT thread will be executed.
#
class InterCallObject:
def __init__(self):
self.finish_event = threading.Event()
self.result = None
def call_on_qt_thread(func):
def _call_on_qt_thread_wrapper(*args, **kwargs):
def _handle_call(ico, *args, **kwargs):
ico.result = func(*args, **kwargs)
ico.finish_event.set()
inter_call_object = InterCallObject()
new_args = tuple([inter_call_object] + list(args)[:])
CuraApplication.getInstance().callLater(_handle_call, *new_args, **kwargs)
inter_call_object.finish_event.wait()
return inter_call_object.result
return _call_on_qt_thread_wrapper
## Base implementation for reading 3MF workspace files.
class ThreeMFWorkspaceReader(WorkspaceReader):
def __init__(self):
@ -89,7 +121,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# The default ContainerStack.deserialize() will connect signals, which is not desired in this case.
# Since we know that the stack files are INI files, so we directly use the ConfigParser to parse them.
serialized = archive.open(file_name).read().decode("utf-8")
stack_config = ConfigParser()
stack_config = ConfigParser(interpolation = None)
stack_config.read_string(serialized)
# sanity check
@ -168,11 +200,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
Logger.log("w", "Unknown definition container type %s for %s",
definition_container_type, each_definition_container_file)
Job.yieldThread()
# sanity check
if machine_definition_container_count != 1:
msg = "Expecting one machine definition container but got %s" % machine_definition_container_count
Logger.log("e", msg)
raise RuntimeError(msg)
return WorkspaceReader.PreReadResult.failed #Not a workspace file but ordinary 3MF.
material_labels = []
material_conflict = False
@ -271,9 +301,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# if the global stack is found, we check if there are conflicts in the extruder stacks
if containers_found_dict["machine"] and not machine_conflict:
for extruder_stack_file in extruder_stack_files:
container_id = self._stripFileToId(extruder_stack_file)
serialized = archive.open(extruder_stack_file).read().decode("utf-8")
parser = configparser.ConfigParser()
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# The check should be done for the extruder stack that's associated with the existing global stack,
@ -303,7 +332,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
break
num_visible_settings = 0
has_visible_settings_string = False
try:
temp_preferences = Preferences()
serialized = archive.open("Cura/preferences.cfg").read().decode("utf-8")
@ -378,7 +406,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
## Overrides an ExtruderStack in the given GlobalStack and returns the new ExtruderStack.
def _overrideExtruderStack(self, global_stack, extruder_file_content, extruder_stack_file):
# Get extruder position first
extruder_config = configparser.ConfigParser()
extruder_config = configparser.ConfigParser(interpolation = None)
extruder_config.read_string(extruder_file_content)
if not extruder_config.has_option("metadata", "position"):
msg = "Could not find 'metadata/position' in extruder stack file"
@ -404,6 +432,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# containing global.cfg / extruder.cfg
#
# \param file_name
@call_on_qt_thread
def read(self, file_name):
archive = zipfile.ZipFile(file_name, "r")
@ -423,6 +452,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
Logger.log("w", "Workspace did not contain visible settings. Leaving visibility unchanged")
else:
global_preferences.setValue("general/visible_settings", visible_settings)
global_preferences.setValue("general/preset_setting_visibility_choice", "Custom")
categories_expanded = temp_preferences.getValue("cura/categories_expanded")
if categories_expanded is None:
@ -461,7 +491,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
global_stack_id_new = self.getNewId(global_stack_id_original)
global_stack_need_rename = True
global_stack_name_new = self._container_registry.uniqueName(global_stack_name_original)
if self._container_registry.findContainerStacksMetadata(name = global_stack_id_original):
global_stack_name_new = self._container_registry.uniqueName(global_stack_name_original)
for each_extruder_stack_file in extruder_stack_files:
old_container_id = self._stripFileToId(each_extruder_stack_file)
@ -527,12 +558,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
user_instance_containers = []
quality_and_definition_changes_instance_containers = []
quality_changes_instance_containers = []
for instance_container_file in instance_container_files:
container_id = self._stripFileToId(instance_container_file)
serialized = archive.open(instance_container_file).read().decode("utf-8")
# HACK! we ignore "quality" and "variant" instance containers!
parser = configparser.ConfigParser()
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
if not parser.has_option("metadata", "type"):
Logger.log("w", "Cannot find metadata/type in %s, ignoring it", instance_container_file)
@ -583,7 +615,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if machine_id:
new_machine_id = self.getNewId(machine_id)
new_id = new_machine_id + "_current_settings"
instance_container.setMetadataEntry("id", new_id)
instance_container.setMetaDataEntry("id", new_id)
instance_container.setName(new_id)
instance_container.setMetaDataEntry("machine", new_machine_id)
containers_to_add.append(instance_container)
@ -608,7 +640,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container.setName(self._container_registry.uniqueName(instance_container.getName()))
new_changes_container_id = self.getNewId(instance_container.getId())
instance_container._id = new_changes_container_id
instance_container.setMetaDataEntry("id", new_changes_container_id)
# TODO: we don't know the following is correct or not, need to verify
# AND REFACTOR!!!
@ -632,6 +664,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# The ID already exists, but nothing in the values changed, so do nothing.
pass
quality_and_definition_changes_instance_containers.append(instance_container)
if container_type == "quality_changes":
quality_changes_instance_containers.append(instance_container)
if container_type == "definition_changes":
definition_changes_extruder_count = instance_container.getProperty("machine_extruder_count", "value")
@ -681,18 +715,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
file_name = global_stack_file)
# Ensure a unique ID and name
stack._id = global_stack_id_new
# Extruder stacks are "bound" to a machine. If we add the machine as a new one, the id of the
# bound machine also needs to change.
if stack.getMetaDataEntry("machine", None):
stack.setMetaDataEntry("machine", global_stack_id_new)
stack.setMetaDataEntry("id", global_stack_id_new)
# Only machines need a new name, stacks may be non-unique
stack.setName(global_stack_name_new)
container_stacks_added.append(stack)
self._container_registry.addContainer(stack)
# self._container_registry.addContainer(stack)
containers_added.append(stack)
else:
Logger.log("e", "Resolve strategy of %s for machine is not supported", self._resolve_strategies["machine"])
@ -710,6 +739,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return
# load extruder stack files
has_extruder_stack_files = len(extruder_stack_files) > 0
empty_quality_container = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
empty_quality_changes_container = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
try:
for extruder_stack_file in extruder_stack_files:
container_id = self._stripFileToId(extruder_stack_file)
@ -730,7 +762,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# deserialize() by setting the metadata, but in the case of ExtruderStack, deserialize()
# also does addExtruder() to its machine stack, so we have to make sure that it's pointing
# to the right machine BEFORE deserialization.
extruder_config = configparser.ConfigParser()
extruder_config = configparser.ConfigParser(interpolation = None)
extruder_config.read_string(extruder_file_content)
extruder_config.set("metadata", "machine", global_stack_id_new)
tmp_string_io = io.StringIO()
@ -740,7 +772,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
stack.deserialize(extruder_file_content, file_name = extruder_stack_file)
# Ensure a unique ID and name
stack._id = new_id
stack.setMetaDataEntry("id", new_id)
self._container_registry.addContainer(stack)
extruder_stacks_added.append(stack)
@ -758,17 +790,33 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# If not extruder stacks were saved in the project file (pre 3.1) create one manually
# We re-use the container registry's addExtruderStackForSingleExtrusionMachine method for this
if not extruder_stacks:
stack = self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder")
if stack:
if self._resolve_strategies["machine"] == "override":
# in case the extruder is newly created (for a single-extrusion machine), we need to override
# the existing extruder stack.
existing_extruder_stack = global_stack.extruders[stack.getMetaDataEntry("position")]
for idx in range(len(_ContainerIndexes.IndexTypeMap)):
existing_extruder_stack.replaceContainer(idx, stack._containers[idx], postpone_emit = True)
extruder_stacks.append(existing_extruder_stack)
else:
extruder_stacks.append(stack)
# If we choose to override a machine but to create a new custom quality profile, the custom quality
# profile is not immediately applied to the global_stack, so this fix for single extrusion machines
# will use the current custom quality profile on the existing machine. The extra optional argument
# in that function is used in this case to specify a new global stack quality_changes container so
# the fix can correctly create and copy over the custom quality settings to the newly created extruder.
new_global_quality_changes = None
if self._resolve_strategies["quality_changes"] == "new" and len(quality_changes_instance_containers) > 0:
new_global_quality_changes = quality_changes_instance_containers[0]
# Depending if the strategy is to create a new or override, the ids must be or not be unique
stack = self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder",
new_global_quality_changes,
create_new_ids = self._resolve_strategies["machine"] == "new")
if new_global_quality_changes is not None:
quality_changes_instance_containers.append(stack.qualityChanges)
quality_and_definition_changes_instance_containers.append(stack.qualityChanges)
if global_stack.quality.getId() in ("empty", "empty_quality"):
stack.quality = empty_quality_container
if self._resolve_strategies["machine"] == "override":
# in case the extruder is newly created (for a single-extrusion machine), we need to override
# the existing extruder stack.
existing_extruder_stack = global_stack.extruders[stack.getMetaDataEntry("position")]
for idx in range(len(_ContainerIndexes.IndexTypeMap)):
existing_extruder_stack.replaceContainer(idx, stack._containers[idx], postpone_emit = True)
extruder_stacks.append(existing_extruder_stack)
else:
extruder_stacks.append(stack)
except:
Logger.logException("w", "We failed to serialize the stack. Trying to clean up.")
@ -777,6 +825,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._container_registry.removeContainer(container.getId())
return
## In case there is a new machine and once the extruders are created, the global stack is added to the registry,
# otherwise the addContainers function in CuraContainerRegistry will create an extruder stack and then creating
# useless files
if self._resolve_strategies["machine"] == "new":
self._container_registry.addContainer(global_stack)
# Check quality profiles to make sure that if one stack has the "not supported" quality profile,
# all others should have the same.
@ -801,30 +854,24 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if machine_extruder_count is not None:
extruder_stacks_in_use = extruder_stacks[:machine_extruder_count]
available_quality = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_stack,
extruder_stacks_in_use)
quality_manager = CuraApplication.getInstance()._quality_manager
all_quality_groups = quality_manager.getQualityGroups(global_stack)
available_quality_types = [qt for qt, qg in all_quality_groups.items() if qg.is_available]
if not has_not_supported:
has_not_supported = not available_quality
has_not_supported = not available_quality_types
quality_has_been_changed = False
if has_not_supported:
empty_quality_container = self._container_registry.findInstanceContainers(id = "empty_quality")[0]
for stack in [global_stack] + extruder_stacks_in_use:
stack.replaceContainer(_ContainerIndexes.Quality, empty_quality_container)
empty_quality_changes_container = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0]
for stack in [global_stack] + extruder_stacks_in_use:
stack.replaceContainer(_ContainerIndexes.QualityChanges, empty_quality_changes_container)
quality_has_been_changed = True
else:
empty_quality_changes_container = self._container_registry.findInstanceContainers(id="empty_quality_changes")[0]
# The machine in the project has non-empty quality and there are usable qualities for this machine.
# We need to check if the current quality_type is still usable for this machine, if not, then the quality
# will be reset to the "preferred quality" if present, otherwise "normal".
available_quality_types = [q.getMetaDataEntry("quality_type") for q in available_quality]
if global_stack.quality.getMetaDataEntry("quality_type") not in available_quality_types:
# We are here because the quality_type specified in the project is not supported any more,
# so we need to switch it to the "preferred quality" if present, otherwise "normal".
@ -888,6 +935,34 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
extruder_stack.quality = new_quality_container
global_stack.quality = new_quality_container
# Now we are checking if the quality in the extruder stacks is the same as in the global. In other case,
# the quality is set to be the same.
definition_id = global_stack.definition.getId()
definition_id = global_stack.definition.getMetaDataEntry("quality_definition", definition_id)
if not parseBool(global_stack.getMetaDataEntry("has_machine_quality", "False")):
definition_id = "fdmprinter"
for extruder_stack in extruder_stacks_in_use:
# If the quality is different in the stacks, then the quality in the global stack is trusted
if extruder_stack.quality.getMetaDataEntry("quality_type") != global_stack.quality.getMetaDataEntry("quality_type"):
search_criteria = {"id": global_stack.quality.getId(),
"type": "quality",
"definition": definition_id}
if global_stack.getMetaDataEntry("has_machine_materials") and extruder_stack.material.getId() not in ("empty", "empty_material"):
search_criteria["material"] = extruder_stack.material.getId()
containers = self._container_registry.findInstanceContainers(**search_criteria)
if containers:
extruder_stack.quality = containers[0]
extruder_stack.qualityChanges = empty_quality_changes_container
else:
Logger.log("e", "Cannot find a suitable quality for extruder [%s].", extruder_stack.getId())
quality_has_been_changed = True
else:
Logger.log("i", "The quality is the same for the global and the extruder stack [%s]", global_stack.quality.getId())
# Replacing the old containers if resolve is "new".
# When resolve is "new", some containers will get renamed, so all the other containers that reference to those
# MUST get updated too.
@ -930,9 +1005,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# sanity checks
# NOTE: The following cases SHOULD NOT happen!!!!
if not old_container:
if old_container.getId() in ("empty_quality_changes", "empty_definition_changes", "empty"):
Logger.log("e", "We try to get [%s] from the global stack [%s] but we got None instead!",
changes_container_type, global_stack.getId())
continue
# Replace the quality/definition changes container if it's in the GlobalStack
# NOTE: we can get an empty container here, but the IDs will not match,
@ -945,26 +1021,29 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
continue
# Replace the quality/definition changes container if it's in one of the ExtruderStacks
for each_extruder_stack in extruder_stacks:
changes_container = None
if changes_container_type == "quality_changes":
changes_container = each_extruder_stack.qualityChanges
elif changes_container_type == "definition_changes":
changes_container = each_extruder_stack.definitionChanges
# sanity checks
# NOTE: The following cases SHOULD NOT happen!!!!
if not changes_container:
Logger.log("e", "We try to get [%s] from the extruder stack [%s] but we got None instead!",
changes_container_type, each_extruder_stack.getId())
# NOTE: we can get an empty container here, but the IDs will not match,
# so this comparison is fine.
if self._id_mapping.get(changes_container.getId()) == new_id:
# Only apply the change if we have loaded extruder stacks from the project
if has_extruder_stack_files:
for each_extruder_stack in extruder_stacks:
changes_container = None
if changes_container_type == "quality_changes":
each_extruder_stack.qualityChanges = each_changes_container
changes_container = each_extruder_stack.qualityChanges
elif changes_container_type == "definition_changes":
each_extruder_stack.definitionChanges = each_changes_container
changes_container = each_extruder_stack.definitionChanges
# sanity checks
# NOTE: The following cases SHOULD NOT happen!!!!
if changes_container.getId() in ("empty_quality_changes", "empty_definition_changes", "empty"):
Logger.log("e", "We try to get [%s] from the extruder stack [%s] but we got None instead!",
changes_container_type, each_extruder_stack.getId())
continue
# NOTE: we can get an empty container here, but the IDs will not match,
# so this comparison is fine.
if self._id_mapping.get(changes_container.getId()) == new_id:
if changes_container_type == "quality_changes":
each_extruder_stack.qualityChanges = each_changes_container
elif changes_container_type == "definition_changes":
each_extruder_stack.definitionChanges = each_changes_container
if self._resolve_strategies["material"] == "new":
# the actual material instance container can have an ID such as
@ -999,12 +1078,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
for stack in extruder_stacks:
stack.setNextStack(global_stack)
stack.containersChanged.emit(stack.getTop())
else:
CuraApplication.getInstance().getMachineManager().activeQualityChanged.emit()
# Actually change the active machine.
Application.getInstance().setGlobalContainerStack(global_stack)
# Notify everything/one that is to notify about changes.
global_stack.containersChanged.emit(global_stack.getTop())
#
# This is scheduled for later is because it depends on the Variant/Material/Qualitiy Managers to have the latest
# data, but those managers will only update upon a container/container metadata changed signal. Because this
# function is running on the main thread (Qt thread), although those "changed" signals have been emitted, but
# they won't take effect until this function is done.
# To solve this, we schedule _updateActiveMachine() for later so it will have the latest data.
CuraApplication.getInstance().callLater(self._updateActiveMachine, global_stack)
# Load all the nodes / meshdata of the workspace
nodes = self._3mf_mesh_reader.read(file_name)
@ -1017,6 +1101,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self.setWorkspaceName(base_file_name)
return nodes
def _updateActiveMachine(self, global_stack):
# Actually change the active machine.
machine_manager = Application.getInstance().getMachineManager()
machine_manager.setActiveMachine(global_stack.getId())
# Notify everything/one that is to notify about changes.
global_stack.containersChanged.emit(global_stack.getTop())
## HACK: Replaces the material container in the given stack with a newly created material container.
# This function is used when the user chooses to resolve material conflicts by creating new ones.
def _replaceStackMaterialWithNew(self, stack, old_new_material_dict):
@ -1048,13 +1140,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# find the old material ID
old_material_id_in_stack = stack.material.getId()
best_matching_old_material_id = None
best_matching_old_meterial_prefix_length = -1
best_matching_old_material_prefix_length = -1
for old_parent_material_id in old_new_material_dict:
if len(old_parent_material_id) < best_matching_old_meterial_prefix_length:
if len(old_parent_material_id) < best_matching_old_material_prefix_length:
continue
if len(old_parent_material_id) <= len(old_material_id_in_stack):
if old_parent_material_id == old_material_id_in_stack[0:len(old_parent_material_id)]:
best_matching_old_meterial_prefix_length = len(old_parent_material_id)
best_matching_old_material_prefix_length = len(old_parent_material_id)
best_matching_old_material_id = old_parent_material_id
if best_matching_old_material_id is None:

View file

@ -390,6 +390,13 @@ UM.Dialog
}
}
function accept() {
manager.closeBackend();
manager.onOkButtonClicked();
base.visible = false;
base.accept();
}
function reject() {
manager.onCancelButtonClicked();
base.visible = false;

View file

@ -57,7 +57,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
# Save Cura version
version_file = zipfile.ZipInfo("Cura/version.ini")
version_config_parser = configparser.ConfigParser()
version_config_parser = configparser.ConfigParser(interpolation = None)
version_config_parser.add_section("versions")
version_config_parser.set("versions", "cura_version", Application.getInstance().getVersion())
version_config_parser.set("versions", "build_type", Application.getInstance().getBuildType())
@ -97,7 +97,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
# Do not include the network authentication keys
ignore_keys = {"network_authentication_id", "network_authentication_key"}
ignore_keys = {"network_authentication_id", "network_authentication_key", "octoprint_api_key"}
serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)
archive.writestr(file_in_archive, serialized_data)

View file

@ -6,7 +6,9 @@ from UM.Math.Vector import Vector
from UM.Logger import Logger
from UM.Math.Matrix import Matrix
from UM.Application import Application
import UM.Scene.SceneNode
from UM.Scene.SceneNode import SceneNode
from cura.CuraApplication import CuraApplication
import Savitar
@ -61,11 +63,15 @@ class ThreeMFWriter(MeshWriter):
self._store_archive = store_archive
## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode
# \returns Uranium Scenen node.
# \returns Uranium Scene node.
def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()):
if type(um_node) is not UM.Scene.SceneNode.SceneNode:
if not isinstance(um_node, SceneNode):
return None
active_build_plate_nr = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
if um_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
return
savitar_node = Savitar.SceneNode()
node_matrix = um_node.getLocalTransformation()
@ -96,6 +102,9 @@ class ThreeMFWriter(MeshWriter):
savitar_node.setSetting(key, str(stack.getProperty(key, "value")))
for child_node in um_node.getChildren():
# only save the nodes on the active build plate
if child_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
continue
savitar_child_node = self._convertUMNodeToSavitarNode(child_node)
if savitar_child_node is not None:
savitar_node.addChild(savitar_child_node)

View file

@ -9,6 +9,7 @@ from UM.Application import Application
from UM.Resources import Resources
from UM.Logger import Logger
class AutoSave(Extension):
def __init__(self):
super().__init__()
@ -16,18 +17,40 @@ class AutoSave(Extension):
Preferences.getInstance().preferenceChanged.connect(self._triggerTimer)
self._global_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10)
self._change_timer = QTimer()
self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay"))
self._change_timer.setSingleShot(True)
self._change_timer.timeout.connect(self._onTimeout)
self._saving = False
# At this point, the Application instance has not finished its constructor call yet, so directly using something
# like Application.getInstance() is not correct. The initialisation now will only gets triggered after the
# application finishes its start up successfully.
self._init_timer = QTimer()
self._init_timer.setInterval(1000)
self._init_timer.setSingleShot(True)
self._init_timer.timeout.connect(self.initialize)
self._init_timer.start()
def initialize(self):
# only initialise if the application is created and has started
from cura.CuraApplication import CuraApplication
if not CuraApplication.Created:
self._init_timer.start()
return
if not CuraApplication.getInstance().started:
self._init_timer.start()
return
self._change_timer.timeout.connect(self._onTimeout)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
self._triggerTimer()
def _triggerTimer(self, *args):
if not self._saving:
self._change_timer.start()

View file

@ -1,3 +1,49 @@
[3.2.0]
*Tree support
Experimental tree-like support structure that uses branches to support prints. Branches grow and multiply towards the model, with fewer contact points than alternative support methods. This results in better surface finishes for organic-shaped prints.
*Adaptive layers
Prints with a variable layer thickness which adapts to the angle of the models surfaces. The result is high-quality surface finishes with a marginally increased print time. This setting can be found under the experimental category.
*Faster startup
Printer definitions are now loaded when adding a printer, instead of loading all available printers on startup.
*Backface culling in layer view
Doubled frame rate by only rendering visible surfaces of the model in the layer view, instead of rendering the entire model. Good for lower spec GPUs as it is less resource-intensive.
*Multi build plate
Experimental feature that creates separate build plates with shared settings in a single session, eliminating the need to clear the build plate multiple times. Multiple build plates can be sliced and sent to a printer or printer group in Cura Connect. This feature must be enabled manually in the preferences general tab.
*Improved mesh type selection
New button in the left toolbar to edit per model settings, giving the user more control over where to place support. Objects can be used as meshes, with a drop down list where Print as support, Don't overlap support with other models, Modify settings for overlap with other models, or Modify settings for infill of other models can be specified. Contributed by fieldOfView.
*View optimization
Quick camera controls introduced in version 3.1 have been revised to create more accurate isometric, front, left, and right views.
*Updated sidebar to QtQuick 2.0
Application framework updated to increase speed, achieve a better width and style fit, and gives users dropdown menus that are styled to fit the enabled Ultimaker Cura theme, instead of the operating systems theme.
*Hide sidebar
The sidebar can now be hidden/shown by selecting View > Expand/Collapse Sidebar, or with the hotkey CMD + E (Mac) or CTRL + E (PC and Linux).
*Disable Send slice information
A shortcut to disable Send slice information has been added to the first launch to make it easier for privacy-conscious users to keep slice information private.
*Signed binaries (Windows)
For security-conscious users, the Windows installer and Windows binaries have been digitally signed to prevent “Unknown application” warnings and virus scanner false-positives.
*Start/end gcode script per extruder
Variables from both extruders in the start and end gcode snippets can now be accessed and edited, creating uniformity between profiles in different slicing environments. Contributed by fieldOfView.
*OctoPrint plugin added to plugin browser
This plugin enables printers managed with OctoPrint to print via Ultimaker Cura interface (version 3.2 or later).
*Bugfixes
- Fixed a bug where the mirror tool and center model options when used together would reset the model transformations
- Updated config file path to fix crashes caused by user config files that are located on remote drives
- Updated Arduino drivers to fix triggering errors during OTA updates in shared environments. This also fixes an issue when upgrading the firmware of the Ultimaker Original.
- Fixed an issue where arranging small models would fail, due to conflict with small model files combined with the “Ensure models are kept apart” option
[3.1.0]
*Profile added for 0.25 mm print core
This new print core gives extra fine line widths which gives prints extra definition and surface quality.

View file

@ -16,6 +16,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Qt.Duration import DurationFormat
from PyQt5.QtCore import QObject, pyqtSlot
from collections import defaultdict
from cura.Settings.ExtruderManager import ExtruderManager
from . import ProcessSlicedLayersJob
from . import StartSliceJob
@ -69,9 +70,10 @@ class CuraEngineBackend(QObject, Backend):
# Workaround to disable layer view processing if layer view is not active.
self._layer_view_active = False
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
Application.getInstance().getMultiBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveViewChanged)
self._onActiveViewChanged()
self._stored_layer_data = []
self._stored_optimized_layer_data = []
self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
self._scene = Application.getInstance().getController().getScene()
self._scene.sceneChanged.connect(self._onSceneChanged)
@ -86,7 +88,6 @@ class CuraEngineBackend(QObject, Backend):
#
self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
Application.getInstance().getExtruderManager().activeExtruderChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
@ -105,17 +106,18 @@ class CuraEngineBackend(QObject, Backend):
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
self._start_slice_job = None
self._start_slice_job_build_plate = None
self._slicing = False # Are we currently slicing?
self._restart = False # Back-end is currently restarting?
self._tool_active = False # If a tool is active, some tasks do not have to do anything
self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers.
self._need_slicing = False
self._build_plates_to_be_sliced = [] # what needs slicing?
self._engine_is_fresh = True # Is the newly started engine used before or not?
self._backend_log_max_lines = 20000 # Maximum number of lines to buffer
self._error_message = None # Pop-up message that shows errors.
self._last_num_objects = 0 # Count number of objects to see if there is something changed
self._last_num_objects = defaultdict(int) # Count number of objects to see if there is something changed
self._postponed_scene_change_sources = [] # scene change is postponed (by a tool)
self.backendQuit.connect(self._onBackendQuit)
@ -174,6 +176,7 @@ class CuraEngineBackend(QObject, Backend):
self._createSocket()
if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon.
Logger.log("d", "Aborting process layers job...")
self._process_layers_job.abort()
self._process_layers_job = None
@ -183,24 +186,42 @@ class CuraEngineBackend(QObject, Backend):
## Manually triggers a reslice
@pyqtSlot()
def forceSlice(self):
if self._use_timer:
self._change_timer.start()
else:
self.slice()
self.markSliceAll()
self.slice()
## Perform a slice of the scene.
def slice(self):
Logger.log("d", "Starting to slice...")
self._slice_start_time = time()
if not self._need_slicing:
if not self._build_plates_to_be_sliced:
self.processingProgress.emit(1.0)
self.backendStateChange.emit(BackendState.Done)
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
return
if Application.getInstance().getPrintInformation():
Application.getInstance().getPrintInformation().setToZeroPrintInformation()
if self._process_layers_job:
Logger.log("d", "Process layers job still busy, trying later.")
return
if not hasattr(self._scene, "gcode_dict"):
self._scene.gcode_dict = {}
# see if we really have to slice
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
num_objects = self._numObjects()
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
self._scene.gcode_dict[build_plate_to_be_sliced] = []
Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced)
if self._build_plates_to_be_sliced:
self.slice()
return
self._stored_layer_data = []
self._stored_optimized_layer_data = []
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
if Application.getInstance().getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
Application.getInstance().getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
if self._process is None:
self._createSocket()
@ -210,12 +231,16 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted)
self._scene.gcode_list = []
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #[] indexed by build plate number
self._slicing = True
self.slicingStarted.emit()
self.determineAutoSlicing() # Switch timer on or off if appropriate
slice_message = self._socket.createMessage("cura.proto.Slice")
self._start_slice_job = StartSliceJob.StartSliceJob(slice_message)
self._start_slice_job_build_plate = build_plate_to_be_sliced
self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate)
self._start_slice_job.start()
self._start_slice_job.finished.connect(self._onStartSliceCompleted)
@ -224,7 +249,8 @@ class CuraEngineBackend(QObject, Backend):
def _terminate(self):
self._slicing = False
self._stored_layer_data = []
self._stored_optimized_layer_data = []
if self._start_slice_job_build_plate in self._stored_optimized_layer_data:
del self._stored_optimized_layer_data[self._start_slice_job_build_plate]
if self._start_slice_job is not None:
self._start_slice_job.cancel()
@ -262,6 +288,7 @@ class CuraEngineBackend(QObject, Backend):
self._start_slice_job = None
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
self.backendStateChange.emit(BackendState.Error)
return
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
@ -339,7 +366,9 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.Error)
else:
self.backendStateChange.emit(BackendState.NotStarted)
self._invokeSlice()
return
# Preparation completed, send it to the backend.
self._socket.sendMessage(job.getSliceMessage())
@ -363,7 +392,7 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.Disabled)
gcode_list = node.callDecoration("getGCodeList")
if gcode_list is not None:
self._scene.gcode_list = gcode_list
self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list
if self._use_timer == enable_timer:
return self._use_timer
@ -375,33 +404,53 @@ class CuraEngineBackend(QObject, Backend):
self.disableTimer()
return False
## Return a dict with number of objects per build plate
def _numObjects(self):
num_objects = defaultdict(int)
for node in DepthFirstIterator(self._scene.getRoot()):
# Only count sliceable objects
if node.callDecoration("isSliceable"):
build_plate_number = node.callDecoration("getBuildPlateNumber")
num_objects[build_plate_number] += 1
return num_objects
## Listener for when the scene has changed.
#
# This should start a slice if the scene is now ready to slice.
#
# \param source The scene node that was changed.
def _onSceneChanged(self, source):
if type(source) is not SceneNode:
if not isinstance(source, SceneNode):
return
root_scene_nodes_changed = False
if source == self._scene.getRoot():
num_objects = 0
for node in DepthFirstIterator(self._scene.getRoot()):
# Only count sliceable objects
if node.callDecoration("isSliceable"):
num_objects += 1
if num_objects != self._last_num_objects:
self._last_num_objects = num_objects
root_scene_nodes_changed = True
else:
return
# This case checks if the source node is a node that contains GCode. In this case the
# current layer data is removed so the previous data is not rendered - CURA-4821
if source.callDecoration("isBlockSlicing") and source.callDecoration("getLayerData"):
self._stored_optimized_layer_data = {}
if not source.callDecoration("isGroup") and not root_scene_nodes_changed:
if source.getMeshData() is None:
return
if source.getMeshData().getVertices() is None:
return
build_plate_changed = set()
source_build_plate_number = source.callDecoration("getBuildPlateNumber")
if source == self._scene.getRoot():
# we got the root node
num_objects = self._numObjects()
for build_plate_number in list(self._last_num_objects.keys()) + list(num_objects.keys()):
if build_plate_number not in self._last_num_objects or num_objects[build_plate_number] != self._last_num_objects[build_plate_number]:
self._last_num_objects[build_plate_number] = num_objects[build_plate_number]
build_plate_changed.add(build_plate_number)
else:
# we got a single scenenode
if not source.callDecoration("isGroup"):
if source.getMeshData() is None:
return
if source.getMeshData().getVertices() is None:
return
build_plate_changed.add(source_build_plate_number)
build_plate_changed.discard(None)
build_plate_changed.discard(-1) # object not on build plate
if not build_plate_changed:
return
if self._tool_active:
# do it later, each source only has to be done once
@ -409,9 +458,18 @@ class CuraEngineBackend(QObject, Backend):
self._postponed_scene_change_sources.append(source)
return
self.needsSlicing()
self.stopSlicing()
self._onChanged()
for build_plate_number in build_plate_changed:
if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number)
self.printDurationMessage.emit(source_build_plate_number, {}, [])
self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted)
# if not self._use_timer:
# With manually having to slice, we want to clear the old invalid layer data.
self._clearLayerData(build_plate_changed)
self._invokeSlice()
## Called when an error occurs in the socket connection towards the engine.
#
@ -431,16 +489,21 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("w", "A socket error caused the connection to be reset")
## Remove old layer data (if any)
def _clearLayerData(self):
def _clearLayerData(self, build_plate_numbers = set()):
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData"):
node.getParent().removeChild(node)
break
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
node.getParent().removeChild(node)
## Convenient function: set need_slicing, emit state and clear layer data
def markSliceAll(self):
for build_plate_number in range(Application.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1):
if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number)
## Convenient function: mark everything to slice, emit state and clear layer data
def needsSlicing(self):
self.stopSlicing()
self._need_slicing = True
self.markSliceAll()
self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted)
if not self._use_timer:
@ -462,7 +525,7 @@ class CuraEngineBackend(QObject, Backend):
def _onStackErrorCheckFinished(self):
self._is_error_check_scheduled = False
if not self._slicing and self._need_slicing:
if not self._slicing and self._build_plates_to_be_sliced:
self.needsSlicing()
self._onChanged()
@ -476,7 +539,9 @@ class CuraEngineBackend(QObject, Backend):
#
# \param message The protobuf message containing sliced layer data.
def _onOptimizedLayerMessage(self, message):
self._stored_optimized_layer_data.append(message)
if self._start_slice_job_build_plate not in self._stored_optimized_layer_data:
self._stored_optimized_layer_data[self._start_slice_job_build_plate] = []
self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message)
## Called when a progress message is received from the engine.
#
@ -485,6 +550,16 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(message.amount)
self.backendStateChange.emit(BackendState.Processing)
# testing
def _invokeSlice(self):
if self._use_timer:
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
# otherwise business as usual
if self._is_error_check_scheduled:
self._change_timer.stop()
else:
self._change_timer.start()
## Called when the engine sends a message that slicing is finished.
#
# \param message The protobuf message signalling that slicing is finished.
@ -492,36 +567,46 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.Done)
self.processingProgress.emit(1.0)
for line in self._scene.gcode_list:
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate]
for index, line in enumerate(gcode_list):
replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths))
replaced = replaced.replace("{filament_weight}", str(Application.getInstance().getPrintInformation().materialWeights))
replaced = replaced.replace("{filament_cost}", str(Application.getInstance().getPrintInformation().materialCosts))
replaced = replaced.replace("{jobname}", str(Application.getInstance().getPrintInformation().jobName))
self._scene.gcode_list[self._scene.gcode_list.index(line)] = replaced
gcode_list[index] = replaced
self._slicing = False
self._need_slicing = False
Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()):
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
self._process_layers_job.start()
self._stored_optimized_layer_data = []
# See if we need to process the sliced layers job.
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate:
self._startProcessSlicedLayersJob(active_build_plate)
# self._onActiveViewChanged()
self._start_slice_job_build_plate = None
Logger.log("d", "See if there is more to slice...")
# Somehow this results in an Arcus Error
# self.slice()
# Call slice again using the timer, allowing the backend to restart
if self._build_plates_to_be_sliced:
self.enableTimer() # manually enable timer to be able to invoke slice, also when in manual slice mode
self._invokeSlice()
## Called when a g-code message is received from the engine.
#
# \param message The protobuf message containing g-code, encoded as UTF-8.
def _onGCodeLayerMessage(self, message):
self._scene.gcode_list.append(message.data.decode("utf-8", "replace"))
self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace"))
## Called when a g-code prefix message is received from the engine.
#
# \param message The protobuf message containing the g-code prefix,
# encoded as UTF-8.
def _onGCodePrefixMessage(self, message):
self._scene.gcode_list.insert(0, message.data.decode("utf-8", "replace"))
self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace"))
## Creates a new socket connection.
def _createSocket(self):
@ -551,7 +636,7 @@ class CuraEngineBackend(QObject, Backend):
material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
times = self._parseMessagePrintTimes(message)
self.printDurationMessage.emit(times, material_amounts)
self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts)
## Called for parsing message to retrieve estimated time per feature
#
@ -605,19 +690,25 @@ class CuraEngineBackend(QObject, Backend):
source = self._postponed_scene_change_sources.pop(0)
self._onSceneChanged(source)
def _startProcessSlicedLayersJob(self, build_plate_number):
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number])
self._process_layers_job.setBuildPlate(build_plate_number)
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
self._process_layers_job.start()
## Called when the user changes the active view mode.
def _onActiveViewChanged(self):
if Application.getInstance().getController().getActiveView():
view = Application.getInstance().getController().getActiveView()
application = Application.getInstance()
view = application.getController().getActiveView()
if view:
active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
self._layer_view_active = True
# There is data and we're not slicing at the moment
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
if self._stored_optimized_layer_data and not self._slicing:
self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data)
self._process_layers_job.finished.connect(self._onProcessLayersFinished)
self._process_layers_job.start()
self._stored_optimized_layer_data = []
# TODO: what build plate I am slicing
if active_build_plate in self._stored_optimized_layer_data and not self._slicing and not self._process_layers_job:
self._startProcessSlicedLayersJob(active_build_plate)
else:
self._layer_view_active = False
@ -635,7 +726,7 @@ class CuraEngineBackend(QObject, Backend):
if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
self._global_container_stack.containersChanged.disconnect(self._onChanged)
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
extruders = list(self._global_container_stack.extruders.values())
for extruder in extruders:
extruder.propertyChanged.disconnect(self._onSettingChanged)
@ -646,14 +737,17 @@ class CuraEngineBackend(QObject, Backend):
if self._global_container_stack:
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
self._global_container_stack.containersChanged.connect(self._onChanged)
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
extruders = list(self._global_container_stack.extruders.values())
for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingChanged)
extruder.containersChanged.connect(self._onChanged)
self._onChanged()
def _onProcessLayersFinished(self, job):
del self._stored_optimized_layer_data[job.getBuildPlate()]
self._process_layers_job = None
Logger.log("d", "See if there is more to slice(2)...")
self._invokeSlice()
## Connect slice function to timer.
def enableTimer(self):

View file

@ -12,4 +12,6 @@ class ProcessGCodeLayerJob(Job):
self._message = message
def run(self):
self._scene.gcode_list.append(self._message.data.decode("utf-8", "replace"))
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_list = self._scene.gcode_dict[active_build_plate_id]
gcode_list.append(self._message.data.decode("utf-8", "replace"))

View file

@ -4,8 +4,6 @@
import gc
from UM.Job import Job
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Application import Application
from UM.Mesh.MeshData import MeshData
from UM.Preferences import Preferences
@ -17,6 +15,8 @@ from UM.Logger import Logger
from UM.Math.Vector import Vector
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager
from cura import LayerDataBuilder
from cura import LayerDataDecorator
@ -49,6 +49,7 @@ class ProcessSlicedLayersJob(Job):
self._scene = Application.getInstance().getController().getScene()
self._progress_message = Message(catalog.i18nc("@info:status", "Processing Layers"), 0, False, -1)
self._abort_requested = False
self._build_plate_number = None
## Aborts the processing of layers.
#
@ -59,7 +60,14 @@ class ProcessSlicedLayersJob(Job):
def abort(self):
self._abort_requested = True
def setBuildPlate(self, new_value):
self._build_plate_number = new_value
def getBuildPlate(self):
return self._build_plate_number
def run(self):
Logger.log("d", "Processing new layer for build plate %s..." % self._build_plate_number)
start_time = time()
view = Application.getInstance().getController().getActiveView()
if view.getPluginId() == "SimulationView":
@ -73,17 +81,8 @@ class ProcessSlicedLayersJob(Job):
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
new_node = SceneNode()
## Remove old layer data (if any)
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData"):
node.getParent().removeChild(node)
break
if self._abort_requested:
if self._progress_message:
self._progress_message.hide()
return
new_node = CuraSceneNode()
new_node.addDecorator(BuildPlateDecorator(self._build_plate_number))
# Force garbage collection.
# For some reason, Python has a tendency to keep the layer data

View file

@ -5,20 +5,25 @@ import numpy
from string import Formatter
from enum import IntEnum
import time
import re
from UM.Job import Job
from UM.Application import Application
from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.Validator import ValidatorState
from UM.Settings.SettingRelation import RelationType
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.OneAtATimeIterator import OneAtATimeIterator
from cura.Settings.ExtruderManager import ExtruderManager
NON_PRINTING_MESH_SETTINGS = ["anti_overhang_mesh", "infill_mesh", "cutting_mesh"]
class StartJobResult(IntEnum):
Finished = 1
Error = 2
@ -32,14 +37,37 @@ class StartJobResult(IntEnum):
## Formatter class that handles token expansion in start/end gcod
class GcodeStartEndFormatter(Formatter):
def get_value(self, key, args, kwargs): # [CodeStyle: get_value is an overridden function from the Formatter class]
# The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key),
# and a default_extruder_nr to use when no extruder_nr is specified
if isinstance(key, str):
try:
return kwargs[key]
extruder_nr = kwargs["default_extruder_nr"]
except ValueError:
extruder_nr = -1
key_fragments = [fragment.strip() for fragment in key.split(',')]
if len(key_fragments) == 2:
try:
extruder_nr = int(key_fragments[1])
except ValueError:
try:
extruder_nr = int(kwargs["-1"][key_fragments[1]]) # get extruder_nr values from the global stack
except (KeyError, ValueError):
# either the key does not exist, or the value is not an int
Logger.log("w", "Unable to determine stack nr '%s' for key '%s' in start/end g-code, using global stack", key_fragments[1], key_fragments[0])
elif len(key_fragments) != 1:
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end g-code", key)
return "{" + str(key) + "}"
key = key_fragments[0]
try:
return kwargs[str(extruder_nr)][key]
except KeyError:
Logger.log("w", "Unable to replace '%s' placeholder in start/end gcode", key)
Logger.log("w", "Unable to replace '%s' placeholder in start/end g-code", key)
return "{" + key + "}"
else:
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end gcode", key)
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end g-code", key)
return "{" + str(key) + "}"
@ -51,10 +79,16 @@ class StartSliceJob(Job):
self._scene = Application.getInstance().getController().getScene()
self._slice_message = slice_message
self._is_cancelled = False
self._build_plate_number = None
self._all_extruders_settings = None # cache for all setting values from all stacks (global & extruder) for the current machine
def getSliceMessage(self):
return self._slice_message
def setBuildPlate(self, build_plate_number):
self._build_plate_number = build_plate_number
## Check if a stack has any errors.
## returns true if it has errors, false otherwise.
def _checkStackForErrors(self, stack):
@ -71,6 +105,10 @@ class StartSliceJob(Job):
## Runs the job that initiates the slicing.
def run(self):
if self._build_plate_number is None:
self.setResult(StartJobResult.Error)
return
stack = Application.getInstance().getGlobalContainerStack()
if not stack:
self.setResult(StartJobResult.Error)
@ -85,6 +123,12 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.BuildPlateError)
return
# Don't slice if the buildplate or the nozzle type is incompatible with the materials
if not Application.getInstance().getMachineManager().variantBuildplateCompatible and \
not Application.getInstance().getMachineManager().variantBuildplateUsable:
self.setResult(StartJobResult.MaterialIncompatible)
return
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
material = extruder_stack.findContainer({"type": "material"})
if material:
@ -92,9 +136,14 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.MaterialIncompatible)
return
# Validate settings per selectable model
if Application.getInstance().getObjectsModel().stacksHaveErrors():
self.setResult(StartJobResult.ObjectSettingError)
return
# Don't slice if there is a per object setting with an error value.
for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is not SceneNode or not node.isSelectable():
if node.isSelectable():
continue
if self._checkStackForErrors(node.callDecoration("getStack")):
@ -104,7 +153,7 @@ class StartSliceJob(Job):
with self._scene.getSceneLock():
# Remove old layer data.
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData"):
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
node.getParent().removeChild(node)
break
@ -118,10 +167,15 @@ class StartSliceJob(Job):
if getattr(node, "_outside_buildarea", False):
continue
# Filter on current build plate
build_plate_number = node.callDecoration("getBuildPlateNumber")
if build_plate_number is not None and build_plate_number != self._build_plate_number:
continue
children = node.getAllChildren()
children.append(node)
for child_node in children:
if type(child_node) is SceneNode and child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
if child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
temp_list.append(child_node)
if temp_list:
@ -133,12 +187,18 @@ class StartSliceJob(Job):
temp_list = []
has_printing_mesh = False
for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
_non_printing_mesh = getattr(node, "_non_printing_mesh", False)
if not getattr(node, "_outside_buildarea", False) or _non_printing_mesh:
temp_list.append(node)
if not _non_printing_mesh:
has_printing_mesh = True
if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None:
per_object_stack = node.callDecoration("getStack")
is_non_printing_mesh = False
if per_object_stack:
is_non_printing_mesh = any(per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS)
if node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
if not getattr(node, "_outside_buildarea", False) or is_non_printing_mesh:
temp_list.append(node)
if not is_non_printing_mesh:
has_printing_mesh = True
Job.yieldThread()
#If the list doesn't have any model with suitable settings then clean the list
@ -224,19 +284,36 @@ class StartSliceJob(Job):
result["date"] = time.strftime("%d-%m-%Y")
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
result["initial_extruder_nr"] = initial_extruder_nr
return result
## Replace setting tokens in a piece of g-code.
# \param value A piece of g-code to replace tokens in.
# \param settings A dictionary of tokens to replace and their respective
# replacement strings.
def _expandGcodeTokens(self, value: str, settings: dict):
# \param default_extruder_nr Stack nr to use when no stack nr is specified, defaults to the global stack
def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1):
if not self._all_extruders_settings:
global_stack = Application.getInstance().getGlobalContainerStack()
# NB: keys must be strings for the string formatter
self._all_extruders_settings = {
"-1": self._buildReplacementTokens(global_stack)
}
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
extruder_nr = extruder_stack.getProperty("extruder_nr", "value")
self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack)
try:
# any setting can be used as a token
fmt = GcodeStartEndFormatter()
settings = self._all_extruders_settings.copy()
settings["default_extruder_nr"] = default_extruder_nr
return str(fmt.format(value, **settings))
except:
Logger.logException("w", "Unable to do token replacement on start/end gcode")
Logger.logException("w", "Unable to do token replacement on start/end g-code")
return str(value)
## Create extruder message from stack
@ -250,8 +327,9 @@ class StartSliceJob(Job):
settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "")
# Replace the setting tokens in start and end g-code.
settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], settings)
settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], settings)
extruder_nr = stack.getProperty("extruder_nr", "value")
settings["machine_extruder_start_code"] = self._expandGcodeTokens(settings["machine_extruder_start_code"], extruder_nr)
settings["machine_extruder_end_code"] = self._expandGcodeTokens(settings["machine_extruder_end_code"], extruder_nr)
for key, value in settings.items():
# Do not send settings that are not settable_per_extruder.
@ -271,18 +349,20 @@ class StartSliceJob(Job):
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
start_gcode = settings["machine_start_gcode"]
bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"}
settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings))
print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"}
settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings))
# Find the correct temperatures from the first used extruder
extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
extruder_0_settings = self._buildReplacementTokens(extruder_stack)
bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"]
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr}
settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None
print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"]
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr}
settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None
# Replace the setting tokens in start and end g-code.
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], extruder_0_settings)
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], extruder_0_settings)
# Use values from the first used extruder by default so we get the expected temperatures
initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr)
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], initial_extruder_nr)
# Add all sub-messages for each individual setting.
for key, value in settings.items():

View file

@ -39,7 +39,7 @@ class CuraProfileReader(ProfileReader):
except zipfile.BadZipFile:
# It must be an older profile from Cura 2.1.
with open(file_name, encoding="utf-8") as fhandle:
with open(file_name, encoding = "utf-8") as fhandle:
serialized = fhandle.read()
return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized, file_name)]
@ -52,10 +52,10 @@ class CuraProfileReader(ProfileReader):
parser = configparser.ConfigParser(interpolation=None)
parser.read_string(serialized)
if not "general" in parser:
if "general" not in parser:
Logger.log("w", "Missing required section 'general'.")
return []
if not "version" in parser["general"]:
if "version" not in parser["general"]:
Logger.log("w", "Missing required 'version' property")
return []

View file

@ -20,7 +20,7 @@ i18n_catalog = i18nCatalog("cura")
# The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
# to change it to work for other applications.
class FirmwareUpdateChecker(Extension):
JEDI_VERSION_URL = "http://software.ultimaker.com/jedi/releases/latest.version"
JEDI_VERSION_URL = "http://software.ultimaker.com/jedi/releases/latest.version?utm_source=cura&utm_medium=software&utm_campaign=resources"
def __init__(self):
super().__init__()
@ -49,7 +49,6 @@ class FirmwareUpdateChecker(Extension):
def _onContainerAdded(self, container):
# Only take care when a new GlobalStack was added
if isinstance(container, GlobalStack):
Logger.log("i", "You have a '%s' in printer list. Let's check the firmware!", container.getId())
self.checkFirmwareVersion(container, True)
## Connect with software.ultimaker.com, load latest.version and check version info.

View file

@ -44,6 +44,8 @@ class FirmwareUpdateCheckerJob(Job):
# Now we just do that if the active printer is Ultimaker 3 or Ultimaker 3 Extended or any
# other Ultimaker 3 that will come in the future
if len(machine_name_parts) >= 2 and machine_name_parts[:2] == ["ultimaker", "3"]:
Logger.log("i", "You have a UM3 in printer list. Let's check the firmware!")
# Nothing to parse, just get the string
# TODO: In the future may be done by parsing a JSON file with diferent version for each printer model
current_version = reader(current_version_file).readline().rstrip()

View file

@ -71,7 +71,7 @@ class GCodeProfileReader(ProfileReader):
try:
json_data = json.loads(serialized)
except Exception as e:
Logger.log("e", "Could not parse serialized JSON data from GCode %s, error: %s", file_name, e)
Logger.log("e", "Could not parse serialized JSON data from g-code %s, error: %s", file_name, e)
return None
profiles = []

View file

@ -1,5 +1,5 @@
{
"name": "GCode Profile Reader",
"name": "G-code Profile Reader",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for importing profiles from g-code files.",

View file

@ -8,16 +8,16 @@ from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Vector import Vector
from UM.Message import Message
from UM.Scene.SceneNode import SceneNode
from cura.Scene.CuraSceneNode import CuraSceneNode
from UM.i18n import i18nCatalog
from UM.Preferences import Preferences
catalog = i18nCatalog("cura")
from cura import LayerDataBuilder
from cura import LayerDataDecorator
from cura.LayerDataDecorator import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.GCodeListDecorator import GCodeListDecorator
from cura.Scene.GCodeListDecorator import GCodeListDecorator
from cura.Settings.ExtruderManager import ExtruderManager
import numpy
@ -292,7 +292,7 @@ class FlavorParser:
# We obtain the filament diameter from the selected printer to calculate line widths
self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
scene_node = SceneNode()
scene_node = CuraSceneNode()
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
# real data to calculate it from.
scene_node.getBoundingBox = self._getNullBoundingBox
@ -418,11 +418,17 @@ class FlavorParser:
self._layer_number += 1
current_path.clear()
material_color_map = numpy.zeros((10, 4), dtype = numpy.float32)
material_color_map = numpy.zeros((8, 4), dtype = numpy.float32)
material_color_map[0, :] = [0.0, 0.7, 0.9, 1.0]
material_color_map[1, :] = [0.7, 0.9, 0.0, 1.0]
material_color_map[2, :] = [0.9, 0.0, 0.7, 1.0]
material_color_map[3, :] = [0.7, 0.0, 0.0, 1.0]
material_color_map[4, :] = [0.0, 0.7, 0.0, 1.0]
material_color_map[5, :] = [0.0, 0.0, 0.7, 1.0]
material_color_map[6, :] = [0.3, 0.3, 0.3, 1.0]
material_color_map[7, :] = [0.7, 0.7, 0.7, 1.0]
layer_mesh = self._layer_data_builder.build(material_color_map)
decorator = LayerDataDecorator.LayerDataDecorator()
decorator = LayerDataDecorator()
decorator.setLayerData(layer_mesh)
scene_node.addDecorator(decorator)
@ -430,7 +436,10 @@ class FlavorParser:
gcode_list_decorator.setGCodeList(gcode_list)
scene_node.addDecorator(gcode_list_decorator)
Application.getInstance().getController().getScene().gcode_list = gcode_list
# gcode_dict stores gcode_lists for a number of build plates.
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_dict = {active_build_plate_id: gcode_list}
Application.getInstance().getController().getScene().gcode_dict = gcode_dict
Logger.log("d", "Finished parsing %s" % file_name)
self._message.hide()

View file

@ -26,7 +26,7 @@ class GCodeReader(MeshReader):
# PreRead is used to get the correct flavor. If not, Marlin is set by default
def preRead(self, file_name, *args, **kwargs):
with open(file_name, "r") as file:
with open(file_name, "r", encoding = "utf-8") as file:
for line in file:
if line[:len(self._flavor_keyword)] == self._flavor_keyword:
try:

View file

@ -5,6 +5,7 @@ from UM.Mesh.MeshWriter import MeshWriter
from UM.Logger import Logger
from UM.Application import Application
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Util import parseBool
from cura.Settings.ExtruderManager import ExtruderManager
@ -56,12 +57,16 @@ class GCodeWriter(MeshWriter):
# file. This must always be text mode.
def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
if mode != MeshWriter.OutputMode.TextMode:
Logger.log("e", "GCode Writer does not support non-text mode.")
Logger.log("e", "GCodeWriter does not support non-text mode.")
return False
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
scene = Application.getInstance().getController().getScene()
gcode_list = getattr(scene, "gcode_list")
if gcode_list:
gcode_dict = getattr(scene, "gcode_dict")
if not gcode_dict:
return False
gcode_list = gcode_dict.get(active_build_plate, None)
if gcode_list is not None:
for gcode in gcode_list:
stream.write(gcode)
# Serialise the current container stack and put it at the end of the file.
@ -103,8 +108,8 @@ class GCodeWriter(MeshWriter):
prefix_length = len(prefix)
container_with_profile = stack.qualityChanges
if not container_with_profile:
Logger.log("e", "No valid quality profile found, not writing settings to GCode!")
if container_with_profile.getId() == "empty_quality_changes":
Logger.log("e", "No valid quality profile found, not writing settings to g-code!")
return ""
flat_global_container = self._createFlattenedContainerInstance(stack.getTop(), container_with_profile)
@ -116,12 +121,20 @@ class GCodeWriter(MeshWriter):
if flat_global_container.getMetaDataEntry("quality_type", None) is None:
flat_global_container.addMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
# Change the default defintion
default_machine_definition = "fdmprinter"
if parseBool(stack.getMetaDataEntry("has_machine_quality", "False")):
default_machine_definition = stack.getMetaDataEntry("quality_definition")
if not default_machine_definition:
default_machine_definition = stack.definition.getId()
flat_global_container.setMetaDataEntry("definition", default_machine_definition)
serialized = flat_global_container.serialize()
data = {"global_quality": serialized}
for extruder in sorted(ExtruderManager.getInstance().getMachineExtruders(stack.getId()), key = lambda k: k.getMetaDataEntry("position")):
for extruder in sorted(stack.extruders.values(), key = lambda k: k.getMetaDataEntry("position")):
extruder_quality = extruder.qualityChanges
if not extruder_quality:
if extruder_quality.getId() == "empty_quality_changes":
Logger.log("w", "No extruder quality profile found, not writing quality for extruder %s to file!", extruder.getId())
continue
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.getTop(), extruder_quality)
@ -136,6 +149,10 @@ class GCodeWriter(MeshWriter):
# Ensure that quality_type is set. (Can happen if we have empty quality changes).
if flat_extruder_quality.getMetaDataEntry("quality_type", None) is None:
flat_extruder_quality.addMetaDataEntry("quality_type", extruder.quality.getMetaDataEntry("quality_type", "normal"))
# Change the default defintion
flat_extruder_quality.setMetaDataEntry("definition", default_machine_definition)
extruder_serialized = flat_extruder_quality.serialize()
data.setdefault("extruder_quality", []).append(extruder_serialized)

View file

@ -13,7 +13,7 @@ def getMetaData():
"mesh_writer": {
"output": [{
"extension": "gcode",
"description": catalog.i18nc("@item:inlistbox", "GCode File"),
"description": catalog.i18nc("@item:inlistbox", "G-code File"),
"mime_type": "text/x-gcode",
"mode": GCodeWriter.GCodeWriter.OutputMode.TextMode
}]

View file

@ -1,8 +1,8 @@
{
"name": "GCode Writer",
"name": "G-code Writer",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Writes GCode to a file.",
"description": "Writes g-code to a file.",
"api": 4,
"i18n-catalog": "cura"
}

View file

@ -8,12 +8,13 @@ from PyQt5.QtCore import Qt
from UM.Mesh.MeshReader import MeshReader
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Scene.SceneNode import SceneNode
from UM.Math.Vector import Vector
from UM.Job import Job
from UM.Logger import Logger
from .ImageReaderUI import ImageReaderUI
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
class ImageReader(MeshReader):
def __init__(self):

View file

@ -1,4 +1,4 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser # For reading the legacy profile INI files.
@ -10,8 +10,10 @@ import os.path # For concatenating the path to the plugin and the relative path
from UM.Application import Application # To get the machine manager to create the new profile in.
from UM.Logger import Logger # Logging errors.
from UM.PluginRegistry import PluginRegistry # For getting the path to this plugin's directory.
from UM.Settings.ContainerRegistry import ContainerRegistry #To create unique profile IDs.
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
from cura.ProfileReader import ProfileReader # The plug-in type to implement.
from cura.Settings.ExtruderManager import ExtruderManager #To get the current extruder definition.
## A plugin that reads profile data from legacy Cura versions.
@ -77,7 +79,9 @@ class LegacyProfileReader(ProfileReader):
raise Exception("Unable to import legacy profile. Multi extrusion is not supported")
Logger.log("i", "Importing legacy profile from file " + file_name + ".")
profile = InstanceContainer("Imported Legacy Profile") # Create an empty profile.
container_registry = ContainerRegistry.getInstance()
profile_id = container_registry.uniqueName("Imported Legacy Profile")
profile = InstanceContainer(profile_id) # Create an empty profile.
parser = configparser.ConfigParser(interpolation = None)
try:
@ -120,8 +124,11 @@ class LegacyProfileReader(ProfileReader):
if "translation" not in dict_of_doom:
Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?")
return None
current_printer_definition = global_container_stack.getBottom()
profile.setDefinition(current_printer_definition.getId())
current_printer_definition = global_container_stack.definition
quality_definition = current_printer_definition.getMetaDataEntry("quality_definition")
if not quality_definition:
quality_definition = current_printer_definition.getId()
profile.setDefinition(quality_definition)
for new_setting in dict_of_doom["translation"]: # Evaluate all new settings that would get a value from the translations.
old_setting_expression = dict_of_doom["translation"][new_setting]
compiled = compile(old_setting_expression, new_setting, "eval")
@ -139,14 +146,13 @@ class LegacyProfileReader(ProfileReader):
if len(profile.getAllKeys()) == 0:
Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.")
# We need to downgrade the container to version 1 (in Cura 2.1) so the upgrade system can correctly upgrade
# it to the latest version.
profile.addMetaDataEntry("type", "profile")
# don't know what quality_type it is based on, so use "normal" by default
profile.addMetaDataEntry("quality_type", "normal")
profile.setName(profile_id)
profile.setDirty(True)
#Serialise and deserialise in order to perform the version upgrade.
parser = configparser.ConfigParser(interpolation=None)
data = profile.serialize()
parser.read_string(data)
@ -159,4 +165,21 @@ class LegacyProfileReader(ProfileReader):
data = stream.getvalue()
profile.deserialize(data)
return profile
# The definition can get reset to fdmprinter during the deserialization's upgrade. Here we set the definition
# again.
profile.setDefinition(quality_definition)
#We need to return one extruder stack and one global stack.
global_container_id = container_registry.uniqueName("Global Imported Legacy Profile")
global_profile = profile.duplicate(new_id = global_container_id, new_name = profile_id) #Needs to have the same name as the extruder profile.
global_profile.setDirty(True)
profile_definition = "fdmprinter"
from UM.Util import parseBool
if parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", "False")):
profile_definition = global_container_stack.getMetaDataEntry("quality_definition")
if not profile_definition:
profile_definition = global_container_stack.definition.getId()
global_profile.setDefinition(profile_definition)
return [global_profile]

View file

@ -7,14 +7,11 @@ from UM.FlameProfiler import pyqtSlot
from cura.MachineAction import MachineAction
from UM.Application import Application
from UM.Preferences import Preferences
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger
from cura.CuraApplication import CuraApplication
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.CuraStackBuilder import CuraStackBuilder
@ -30,13 +27,14 @@ class MachineSettingsAction(MachineAction):
self._qml_url = "MachineSettingsAction.qml"
self._global_container_stack = None
self._container_index = 0
from cura.Settings.CuraContainerStack import _ContainerIndexes
self._container_index = _ContainerIndexes.DefinitionChanges
self._container_registry = ContainerRegistry.getInstance()
self._container_registry.containerAdded.connect(self._onContainerAdded)
self._container_registry.containerRemoved.connect(self._onContainerRemoved)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
self._empty_container = self._container_registry.getEmptyInstanceContainer()
@ -67,7 +65,9 @@ class MachineSettingsAction(MachineAction):
self._global_container_stack, self._global_container_stack.getName() + "_settings")
# Notify the UI in which container to store the machine settings data
container_index = self._global_container_stack.getContainerIndex(definition_changes_container)
from cura.Settings.CuraContainerStack import CuraContainerStack, _ContainerIndexes
container_index = _ContainerIndexes.DefinitionChanges
if container_index != self._container_index:
self._container_index = container_index
self.containerIndexChanged.emit()
@ -82,17 +82,6 @@ class MachineSettingsAction(MachineAction):
if self._backend and self._backend.determineAutoSlicing():
self._backend.tickle()
def _onActiveExtruderStackChanged(self):
extruder_container_stack = ExtruderManager.getInstance().getActiveExtruderStack()
if not self._global_container_stack or not extruder_container_stack:
return
# Make sure there is a definition_changes container to store the machine settings
definition_changes_container = extruder_container_stack.definitionChanges
if definition_changes_container == self._empty_container:
definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer(
extruder_container_stack, extruder_container_stack.getId() + "_settings")
containerIndexChanged = pyqtSignal()
@pyqtProperty(int, notify = containerIndexChanged)
@ -116,60 +105,9 @@ class MachineSettingsAction(MachineAction):
@pyqtSlot(int)
def setMachineExtruderCount(self, extruder_count):
extruder_manager = Application.getInstance().getExtruderManager()
definition_changes_container = self._global_container_stack.definitionChanges
if not self._global_container_stack or definition_changes_container == self._empty_container:
return
previous_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
if extruder_count == previous_extruder_count:
return
# reset all extruder number settings whose value is no longer valid
for setting_instance in self._global_container_stack.userChanges.findInstances():
setting_key = setting_instance.definition.key
if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"):
continue
old_value = int(self._global_container_stack.userChanges.getProperty(setting_key, "value"))
if old_value >= extruder_count:
self._global_container_stack.userChanges.removeInstance(setting_key)
Logger.log("d", "Reset [%s] because its old value [%s] is no longer valid ", setting_key, old_value)
# Check to see if any objects are set to print with an extruder that will no longer exist
root_node = Application.getInstance().getController().getScene().getRoot()
for node in DepthFirstIterator(root_node):
if node.getMeshData():
extruder_nr = node.callDecoration("getActiveExtruderPosition")
if extruder_nr is not None and int(extruder_nr) > extruder_count - 1:
node.callDecoration("setActiveExtruder", extruder_manager.getExtruderStack(extruder_count - 1).getId())
definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count)
# Make sure one of the extruder stacks is active
extruder_manager.setActiveExtruderIndex(0)
# Move settable_per_extruder values out of the global container
# After CURA-4482 this should not be the case anymore, but we still want to support older project files.
global_user_container = self._global_container_stack.getTop()
if previous_extruder_count == 1:
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
global_user_container = self._global_container_stack.getTop()
for setting_instance in global_user_container.findInstances():
setting_key = setting_instance.definition.key
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_extruder:
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
extruder_stack = extruder_stacks[max(0, limit_to_extruder)]
extruder_stack.getTop().setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
global_user_container.removeInstance(setting_key)
self.forceUpdate()
# Note: this method was in this class before, but since it's quite generic and other plugins also need it
# it was moved to the machine manager instead. Now this method just calls the machine manager.
Application.getInstance().getMachineManager().setActiveMachineExtruderCount(extruder_count)
@pyqtSlot()
def forceUpdate(self):
@ -217,79 +155,7 @@ class MachineSettingsAction(MachineAction):
Application.getInstance().globalContainerStackChanged.emit()
@pyqtSlot()
def updateMaterialForDiameter(self):
@pyqtSlot(int)
def updateMaterialForDiameter(self, extruder_position: int):
# Updates the material container to a material that matches the material diameter set for the printer
if not self._global_container_stack:
return
if not self._global_container_stack.getMetaDataEntry("has_materials", False):
return
material = ExtruderManager.getInstance().getActiveExtruderStack().material
material_diameter = material.getProperty("material_diameter", "value")
if not material_diameter:
# in case of "empty" material
material_diameter = 0
material_approximate_diameter = str(round(material_diameter))
definition_changes = self._global_container_stack.definitionChanges
machine_diameter = definition_changes.getProperty("material_diameter", "value")
if not machine_diameter:
machine_diameter = self._global_container_stack.definition.getProperty("material_diameter", "value")
machine_approximate_diameter = str(round(machine_diameter))
if material_approximate_diameter != machine_approximate_diameter:
Logger.log("i", "The the currently active material(s) do not match the diameter set for the printer. Finding alternatives.")
stacks = ExtruderManager.getInstance().getExtruderStacks()
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
materials_definition = self._global_container_stack.definition.getId()
has_material_variants = self._global_container_stack.getMetaDataEntry("has_variants", False)
else:
materials_definition = "fdmprinter"
has_material_variants = False
for stack in stacks:
old_material = stack.material
search_criteria = {
"type": "material",
"approximate_diameter": machine_approximate_diameter,
"material": old_material.getMetaDataEntry("material", "value"),
"supplier": old_material.getMetaDataEntry("supplier", "value"),
"color_name": old_material.getMetaDataEntry("color_name", "value"),
"definition": materials_definition
}
if has_material_variants:
search_criteria["variant"] = stack.variant.getId()
if old_material == self._empty_container:
search_criteria.pop("material", None)
search_criteria.pop("supplier", None)
search_criteria.pop("definition", None)
search_criteria["id"] = stack.getMetaDataEntry("preferred_material")
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Same material with new diameter is not found, search for generic version of the same material type
search_criteria.pop("supplier", None)
search_criteria["color_name"] = "Generic"
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Generic material with new diameter is not found, search for preferred material
search_criteria.pop("color_name", None)
search_criteria.pop("material", None)
search_criteria["id"] = stack.getMetaDataEntry("preferred_material")
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Preferred material with new diameter is not found, search for any material
search_criteria.pop("id", None)
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Just use empty material as a final fallback
materials = [self._empty_container]
Logger.log("i", "Selecting new material: %s" % materials[0].getId())
stack.material = materials[0]
Application.getInstance().getExtruderManager().updateMaterialForDiameter(extruder_position)

View file

@ -70,8 +70,8 @@ Cura.MachineAction
anchors.top: pageTitle.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
property real columnWidth: ((width - 3 * UM.Theme.getSize("default_margin").width) / 2) | 0
property real labelColumnWidth: columnWidth * 0.5
property real columnWidth: Math.round((width - 3 * UM.Theme.getSize("default_margin").width) / 2)
property real labelColumnWidth: Math.round(columnWidth / 2)
Tab
{
@ -165,7 +165,7 @@ Cura.MachineAction
id: gcodeFlavorComboBox
sourceComponent: comboBoxWithOptions
property string settingKey: "machine_gcode_flavor"
property string label: catalog.i18nc("@label", "Gcode flavor")
property string label: catalog.i18nc("@label", "G-code flavor")
property bool forceUpdateOnChange: true
property var afterOnActivate: manager.updateHasMaterialsMetadata
}
@ -270,6 +270,20 @@ Cura.MachineAction
}
}
}
Connections
{
target: manager
onDefinedExtruderCountChanged:
{
extruderCountModel.clear();
for(var i = 0; i < manager.definedExtruderCount; ++i)
{
extruderCountModel.append({text: String(i + 1), value: i});
}
}
}
currentIndex: machineExtruderCountProvider.properties.value - 1
onActivated:
{
@ -278,18 +292,6 @@ Cura.MachineAction
}
}
}
Loader
{
id: materialDiameterField
visible: Cura.MachineManager.hasMaterials
sourceComponent: numericTextFieldWithUnit
property string settingKey: "material_diameter"
property string unit: catalog.i18nc("@label", "mm")
property string tooltip: catalog.i18nc("@tooltip", "The nominal diameter of filament supported by the printer. The exact diameter will be overridden by the material and/or the profile.")
property var afterOnEditingFinished: manager.updateMaterialForDiameter
property string label: catalog.i18nc("@label", "Material diameter")
}
}
}
@ -305,7 +307,7 @@ Cura.MachineAction
width: settingsTabs.columnWidth
Label
{
text: catalog.i18nc("@label", "Start Gcode")
text: catalog.i18nc("@label", "Start G-code")
font.bold: true
}
Loader
@ -315,7 +317,7 @@ Cura.MachineAction
property int areaWidth: parent.width
property int areaHeight: parent.height - y
property string settingKey: "machine_start_gcode"
property string tooltip: catalog.i18nc("@tooltip", "Gcode commands to be executed at the very start.")
property string tooltip: catalog.i18nc("@tooltip", "G-code commands to be executed at the very start.")
}
}
@ -324,7 +326,7 @@ Cura.MachineAction
width: settingsTabs.columnWidth
Label
{
text: catalog.i18nc("@label", "End Gcode")
text: catalog.i18nc("@label", "End G-code")
font.bold: true
}
Loader
@ -334,7 +336,7 @@ Cura.MachineAction
property int areaWidth: parent.width
property int areaHeight: parent.height - y
property string settingKey: "machine_end_gcode"
property string tooltip: catalog.i18nc("@tooltip", "Gcode commands to be executed at the very end.")
property string tooltip: catalog.i18nc("@tooltip", "G-code commands to be executed at the very end.")
}
}
}
@ -346,7 +348,6 @@ Cura.MachineAction
if(currentIndex > 0)
{
contentItem.forceActiveFocus();
Cura.ExtruderManager.setActiveExtruderIndex(currentIndex - 1);
}
}
@ -383,6 +384,25 @@ Cura.MachineAction
property bool isExtruderSetting: true
}
Loader
{
id: materialDiameterField
visible: Cura.MachineManager.hasMaterials
sourceComponent: numericTextFieldWithUnit
property string settingKey: "material_diameter"
property string label: catalog.i18nc("@label", "Compatible material diameter")
property string unit: catalog.i18nc("@label", "mm")
property string tooltip: catalog.i18nc("@tooltip", "The nominal diameter of filament supported by the printer. The exact diameter will be overridden by the material and/or the profile.")
function afterOnEditingFinished()
{
if (settingsTabs.currentIndex > 0)
{
manager.updateMaterialForDiameter(settingsTabs.currentIndex - 1);
}
}
property bool isExtruderSetting: true
}
Loader
{
id: extruderOffsetXField
@ -421,7 +441,7 @@ Cura.MachineAction
width: settingsTabs.columnWidth
Label
{
text: catalog.i18nc("@label", "Extruder Start Gcode")
text: catalog.i18nc("@label", "Extruder Start G-code")
font.bold: true
}
Loader
@ -432,14 +452,14 @@ Cura.MachineAction
property int areaHeight: parent.height - y
property string settingKey: "machine_extruder_start_code"
property bool isExtruderSetting: true
}
}
}
Column {
height: parent.height
width: settingsTabs.columnWidth
Label
{
text: catalog.i18nc("@label", "Extruder End Gcode")
text: catalog.i18nc("@label", "Extruder End G-code")
font.bold: true
}
Loader
@ -481,7 +501,7 @@ Cura.MachineAction
{
if(settingsTabs.currentIndex > 0)
{
return Cura.MachineManager.activeStackId;
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
}
return "";
}
@ -499,11 +519,11 @@ Cura.MachineAction
checked: String(propertyProvider.properties.value).toLowerCase() != 'false'
onClicked:
{
propertyProvider.setPropertyValue("value", checked);
if(_forceUpdateOnChange)
{
manager.forceUpdate();
}
propertyProvider.setPropertyValue("value", checked);
if(_forceUpdateOnChange)
{
manager.forceUpdate();
}
}
}
}
@ -534,7 +554,7 @@ Cura.MachineAction
{
if(settingsTabs.currentIndex > 0)
{
return Cura.MachineManager.activeStackId;
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
}
return "";
}
@ -567,8 +587,11 @@ Cura.MachineAction
TextField
{
id: textField
text: (propertyProvider.properties.value) ? propertyProvider.properties.value : ""
validator: RegExpValidator { regExp: _allowNegative ? /-?[0-9\.]{0,6}/ : /[0-9\.]{0,6}/ }
text: {
const value = propertyProvider.properties.value;
return value ? value : "";
}
validator: RegExpValidator { regExp: _allowNegative ? /-?[0-9\.,]{0,6}/ : /[0-9\.,]{0,6}/ }
onEditingFinished:
{
if (propertyProvider && text != propertyProvider.properties.value)
@ -576,12 +599,7 @@ Cura.MachineAction
propertyProvider.setPropertyValue("value", text);
if(_forceUpdateOnChange)
{
var extruderIndex = Cura.ExtruderManager.activeExtruderIndex;
manager.forceUpdate();
if(Cura.ExtruderManager.activeExtruderIndex != extruderIndex)
{
Cura.ExtruderManager.setActiveExtruderIndex(extruderIndex)
}
}
if(_afterOnEditingFinished)
{
@ -627,7 +645,7 @@ Cura.MachineAction
{
if(settingsTabs.currentIndex > 0)
{
return Cura.MachineManager.activeStackId;
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
}
return "";
}
@ -714,7 +732,7 @@ Cura.MachineAction
width: gcodeArea.width
text: _tooltip
property bool _isExtruderSetting: (typeof(isExtruderSetting) === 'undefined') ? false: isExtruderSetting
property bool _isExtruderSetting: (typeof(isExtruderSetting) === 'undefined') ? false : isExtruderSetting
property string _tooltip: (typeof(tooltip) === 'undefined') ? propertyProvider.properties.description : tooltip
UM.SettingPropertyProvider
@ -726,7 +744,7 @@ Cura.MachineAction
{
if(settingsTabs.currentIndex > 0)
{
return Cura.MachineManager.activeStackId;
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
}
return "";
}
@ -808,10 +826,10 @@ Cura.MachineAction
printHeadPolygon[axis][side] = result;
return result;
}
validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ }
validator: RegExpValidator { regExp: /[0-9\.,]{0,6}/ }
onEditingFinished:
{
printHeadPolygon[axis][side] = parseFloat(textField.text);
printHeadPolygon[axis][side] = parseFloat(textField.text.replace(',','.'));
var polygon = [];
polygon.push([-printHeadPolygon["x"]["min"], printHeadPolygon["y"]["max"]]);
polygon.push([-printHeadPolygon["x"]["min"],-printHeadPolygon["y"]["min"]]);

View file

@ -8,8 +8,9 @@ import Cura 1.0 as Cura
Item
{
width: parent.width
height: parent.height
// parent could be undefined as this component is not visible at all times
width: parent ? parent.width : 0
height: parent ? parent.height : 0
// We show a nice overlay on the 3D viewer when the current output device has no monitor view
Rectangle

View file

@ -14,60 +14,124 @@ class MonitorStage(CuraStage):
super().__init__(parent)
# Wait until QML engine is created, otherwise creating the new QML components will fail
Application.getInstance().engineCreatedSignal.connect(self._setComponents)
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
self._printer_output_device = None
# Update the status icon when the output device is changed
Application.getInstance().getOutputDeviceManager().activeDeviceChanged.connect(self._setIconSource)
self._active_print_job = None
self._active_printer = None
def _setComponents(self):
self._setMainOverlay()
self._setSidebar()
self._setIconSource()
def _setActivePrintJob(self, print_job):
if self._active_print_job != print_job:
if self._active_print_job:
self._active_print_job.stateChanged.disconnect(self._updateIconSource)
self._active_print_job = print_job
if self._active_print_job:
self._active_print_job.stateChanged.connect(self._updateIconSource)
def _setMainOverlay(self):
# Ensure that the right icon source is returned.
self._updateIconSource()
def _setActivePrinter(self, printer):
if self._active_printer != printer:
if self._active_printer:
self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged)
self._active_printer = printer
if self._active_printer:
self._setActivePrintJob(self._active_printer.activePrintJob)
# Jobs might change, so we need to listen to it's changes.
self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged)
else:
self._setActivePrintJob(None)
# Ensure that the right icon source is returned.
self._updateIconSource()
def _onActivePrintJobChanged(self):
self._setActivePrintJob(self._active_printer.activePrintJob)
def _onActivePrinterChanged(self):
self._setActivePrinter(self._printer_output_device.activePrinter)
def _onOutputDevicesChanged(self):
try:
# We assume that you are monitoring the device with the highest priority.
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
if new_output_device != self._printer_output_device:
if self._printer_output_device:
self._printer_output_device.acceptsCommandsChanged.disconnect(self._updateIconSource)
self._printer_output_device.connectionStateChanged.disconnect(self._updateIconSource)
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
self._printer_output_device = new_output_device
self._printer_output_device.acceptsCommandsChanged.connect(self._updateIconSource)
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
self._printer_output_device.connectionStateChanged.connect(self._updateIconSource)
self._setActivePrinter(self._printer_output_device.activePrinter)
# Force an update of the icon source
self._updateIconSource()
except IndexError:
#If index error occurs, then the icon on monitor button also should be updated
self._updateIconSource()
pass
def _onEngineCreated(self):
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
self._onOutputDevicesChanged()
self._updateMainOverlay()
self._updateSidebar()
self._updateIconSource()
def _updateMainOverlay(self):
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
self.addDisplayComponent("main", main_component_path)
def _setSidebar(self):
def _updateSidebar(self):
# TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor!
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path)
def _setIconSource(self):
def _updateIconSource(self):
if Application.getInstance().getTheme() is not None:
icon_name = self._getActiveOutputDeviceStatusIcon()
self.setIconSource(Application.getInstance().getTheme().getIcon(icon_name))
## Find the correct status icon depending on the active output device state
def _getActiveOutputDeviceStatusIcon(self):
output_device = Application.getInstance().getOutputDeviceManager().getActiveDevice()
if not output_device:
# We assume that you are monitoring the device with the highest priority.
try:
output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
except IndexError:
return "tab_status_unknown"
if hasattr(output_device, "acceptsCommands") and not output_device.acceptsCommands:
if not output_device.acceptsCommands:
return "tab_status_unknown"
if not hasattr(output_device, "printerState") or not hasattr(output_device, "jobState"):
return "tab_status_unknown"
# TODO: refactor to use enum instead of hardcoded strings?
if output_device.printerState == "maintenance":
return "tab_status_busy"
if output_device.jobState in ["printing", "pre_print", "pausing", "resuming"]:
return "tab_status_busy"
if output_device.jobState == "wait_cleanup":
return "tab_status_finished"
if output_device.jobState in ["ready", ""]:
if output_device.activePrinter is None:
return "tab_status_connected"
if output_device.jobState == "paused":
# TODO: refactor to use enum instead of hardcoded strings?
if output_device.activePrinter.state == "maintenance":
return "tab_status_busy"
if output_device.activePrinter.activePrintJob is None:
return "tab_status_connected"
if output_device.activePrinter.activePrintJob.state in ["printing", "pre_print", "pausing", "resuming"]:
return "tab_status_busy"
if output_device.activePrinter.activePrintJob.state == "wait_cleanup":
return "tab_status_finished"
if output_device.activePrinter.activePrintJob.state in ["ready", ""]:
return "tab_status_connected"
if output_device.activePrinter.activePrintJob.state == "paused":
return "tab_status_paused"
if output_device.jobState == "error":
if output_device.activePrinter.activePrintJob.state == "error":
return "tab_status_stopped"
return "tab_status_unknown"

View file

@ -25,7 +25,20 @@ UM.TooltipArea
onClicked:
{
addedSettingsModel.setVisible(model.key, checked);
// Important first set visible and then subscribe
// otherwise the setting is not yet in list
// For unsubscribe is important first remove the subscription and then
// set as invisible
if(checked)
{
addedSettingsModel.setVisible(model.key, checked);
UM.ActiveTool.triggerActionWithData("subscribeForSettingValidation", model.key)
}
else
{
UM.ActiveTool.triggerActionWithData("unsubscribeForSettingValidation", model.key)
addedSettingsModel.setVisible(model.key, checked);
}
UM.ActiveTool.forceUpdate();
}
}

View file

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
@ -22,6 +23,9 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
self._node = None
self._stack = None
# this is a set of settings that will be skipped if the user chooses to reset.
self._skip_reset_setting_set = set()
def setSelectedObjectId(self, id):
if id != self._selected_object_id:
self._selected_object_id = id
@ -36,6 +40,10 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
def selectedObjectId(self):
return self._selected_object_id
@pyqtSlot(str)
def addSkipResetSetting(self, setting_name):
self._skip_reset_setting_set.add(setting_name)
def setVisible(self, visible):
if not self._node:
return
@ -50,6 +58,9 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
# Remove all instances that are not in visibility list
for instance in all_instances:
# exceptionally skip setting
if instance.definition.key in self._skip_reset_setting_set:
continue
if instance.definition.key not in visible:
settings.removeInstance(instance.definition.key)
visibility_changed = True

View file

@ -18,6 +18,9 @@ Item {
width: childrenRect.width;
height: childrenRect.height;
property var all_categories_except_support: [ "machine_settings", "resolution", "shell", "infill", "material", "speed",
"travel", "cooling", "platform_adhesion", "dual", "meshfix", "blackmagic", "experimental"]
Column
{
id: items
@ -39,6 +42,13 @@ Item {
verticalAlignment: Text.AlignVCenter
}
UM.SettingPropertyProvider
{
id: meshTypePropertyProvider
containerStackId: Cura.MachineManager.activeMachineId
watchedProperties: [ "enabled" ]
}
ComboBox
{
id: meshTypeSelection
@ -49,36 +59,55 @@ Item {
model: ListModel
{
id: meshTypeModel
Component.onCompleted:
Component.onCompleted: meshTypeSelection.populateModel()
}
function populateModel()
{
meshTypeModel.append({
type: "",
text: catalog.i18nc("@label", "Normal model")
});
meshTypePropertyProvider.key = "support_mesh";
if(meshTypePropertyProvider.properties.enabled == "True")
{
meshTypeModel.append({
type: "",
text: catalog.i18nc("@label", "Normal model")
});
meshTypeModel.append({
type: "support_mesh",
text: catalog.i18nc("@label", "Print as support")
});
}
meshTypePropertyProvider.key = "anti_overhang_mesh";
if(meshTypePropertyProvider.properties.enabled == "True")
{
meshTypeModel.append({
type: "anti_overhang_mesh",
text: catalog.i18nc("@label", "Don't support overlap with other models")
});
}
meshTypePropertyProvider.key = "cutting_mesh";
if(meshTypePropertyProvider.properties.enabled == "True")
{
meshTypeModel.append({
type: "cutting_mesh",
text: catalog.i18nc("@label", "Modify settings for overlap with other models")
});
}
meshTypePropertyProvider.key = "infill_mesh";
if(meshTypePropertyProvider.properties.enabled == "True")
{
meshTypeModel.append({
type: "infill_mesh",
text: catalog.i18nc("@label", "Modify settings for infill of other models")
});
meshTypeSelection.updateCurrentIndex();
}
meshTypeSelection.updateCurrentIndex();
}
function updateCurrentIndex()
{
var mesh_type = UM.ActiveTool.properties.getValue("MeshType");
meshTypeSelection.currentIndex = -1;
for(var index=0; index < meshTypeSelection.model.count; index++)
{
if(meshTypeSelection.model.get(index).type == mesh_type)
@ -91,6 +120,16 @@ Item {
}
}
Connections
{
target: Cura.MachineManager
onGlobalContainerChanged:
{
meshTypeSelection.model.clear();
meshTypeSelection.populateModel();
}
}
Connections
{
target: UM.Selection
@ -106,12 +145,12 @@ Item {
id: currentSettings
property int maximumHeight: 200 * screenScaleFactor
height: Math.min(contents.count * (UM.Theme.getSize("section").height + UM.Theme.getSize("default_lining").height), maximumHeight)
visible: ["support_mesh", "anti_overhang_mesh"].indexOf(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type) == -1
visible: meshTypeSelection.model.get(meshTypeSelection.currentIndex).type != "anti_overhang_mesh"
ScrollView
{
height: parent.height
width: UM.Theme.getSize("setting").width
width: UM.Theme.getSize("setting").width + UM.Theme.getSize("default_margin").width
style: UM.Theme.styles.scrollview
ListView
@ -124,7 +163,15 @@ Item {
id: addedSettingsModel;
containerId: Cura.MachineManager.activeDefinitionId
expanded: [ "*" ]
exclude: [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
exclude: {
var excluded_settings = [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ];
if(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh")
{
excluded_settings = excluded_settings.concat(base.all_categories_except_support);
}
return excluded_settings;
}
visibilityHandler: Cura.PerObjectSettingVisibilityHandler
{
@ -190,10 +237,13 @@ Item {
Button
{
width: Math.floor(UM.Theme.getSize("setting").height / 2)
width: Math.round(UM.Theme.getSize("setting").height / 2)
height: UM.Theme.getSize("setting").height
onClicked: addedSettingsModel.setVisible(model.key, false)
onClicked: {
addedSettingsModel.setVisible(model.key, false)
UM.ActiveTool.triggerActionWithData("unsubscribeForSettingValidation", model.key)
}
style: ButtonStyle
{
@ -306,7 +356,18 @@ Item {
}
}
onClicked: settingPickDialog.visible = true;
onClicked:
{
settingPickDialog.visible = true;
if (meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh")
{
settingPickDialog.additional_excluded_settings = base.all_categories_except_support;
}
else
{
settingPickDialog.additional_excluded_settings = []
}
}
}
}
@ -315,15 +376,18 @@ Item {
id: settingPickDialog
title: catalog.i18nc("@title:window", "Select Settings to Customize for this model")
width: screenScaleFactor * 360;
width: screenScaleFactor * 360
property string labelFilter: ""
property var additional_excluded_settings
onVisibilityChanged:
{
// force updating the model to sync it with addedSettingsModel
if(visible)
{
// Set skip setting, it will prevent from resetting selected mesh_type
contents.model.visibilityHandler.addSkipResetSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type)
listview.model.forceUpdate()
}
}
@ -394,7 +458,12 @@ Item {
}
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
expanded: [ "*" ]
exclude: [ "machine_settings", "command_line_settings", "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ]
exclude:
{
var excluded_settings = [ "machine_settings", "command_line_settings", "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ];
excluded_settings = excluded_settings.concat(settingPickDialog.additional_excluded_settings);
return excluded_settings;
}
}
delegate:Loader
{

View file

@ -10,7 +10,10 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
from cura.Settings.ExtruderManager import ExtruderManager
from UM.Settings.SettingInstance import SettingInstance
from UM.Event import Event
from UM.Settings.Validator import ValidatorState
from UM.Logger import Logger
from PyQt5.QtCore import QTimer
## This tool allows the user to add & change settings per node in the scene.
# The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
@ -34,6 +37,12 @@ class PerObjectSettingsTool(Tool):
self._onGlobalContainerChanged()
Selection.selectionChanged.connect(self._updateEnabled)
self._scene = Application.getInstance().getController().getScene()
self._error_check_timer = QTimer()
self._error_check_timer.setInterval(250)
self._error_check_timer.setSingleShot(True)
self._error_check_timer.timeout.connect(self._updateStacksHaveErrors)
def event(self, event):
super().event(event)
@ -142,3 +151,65 @@ class PerObjectSettingsTool(Tool):
else:
self._single_model_selected = True
Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, self._advanced_mode and self._single_model_selected)
def _onPropertyChanged(self, key: str, property_name: str) -> None:
if property_name == "validationState":
# self._error_check_timer.start()
return
def _updateStacksHaveErrors(self) -> None:
return
# self._checkStacksHaveErrors()
def _checkStacksHaveErrors(self):
for node in DepthFirstIterator(self._scene.getRoot()):
# valdiate only objects which can be selected because the settings per object
# can be applied only for them
if not node.isSelectable():
continue
hasErrors = self._checkStackForErrors(node.callDecoration("getStack"))
Application.getInstance().getObjectsModel().setStacksHaveErrors(hasErrors)
#If any of models has an error then no reason check next objects on the build plate
if hasErrors:
break
def _checkStackForErrors(self, stack):
print("checking for errors")
if stack is None:
return False
for key in stack.getAllKeys():
validation_state = stack.getProperty(key, "validationState")
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
Logger.log("w", "Setting Per Object %s is not valid.", key)
return True
return False
def subscribeForSettingValidation(self, setting_name):
selected_object = Selection.getSelectedObject(0)
stack = selected_object.callDecoration("getStack") # Don't try to get the active extruder since it may be None anyway.
if not stack:
return ""
settings = stack.getTop()
setting_instance = settings.getInstance(setting_name)
if setting_instance:
setting_instance.propertyChanged.connect(self._onPropertyChanged)
def unsubscribeForSettingValidation(self, setting_name):
selected_object = Selection.getSelectedObject(0)
stack = selected_object.callDecoration("getStack") # Don't try to get the active extruder since it may be None anyway.
if not stack:
return ""
settings = stack.getTop()
setting_instance = settings.getInstance(setting_name)
if setting_instance:
setting_instance.propertyChanged.disconnect(self._onPropertyChanged)

View file

@ -1,31 +1,36 @@
# Copyright (c) 2017 Ultimaker B.V.
# PluginBrowser is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from UM.Application import Application
from UM.Qt.ListModel import ListModel
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.Qt.Bindings.PluginsModel import PluginsModel
from UM.Extension import Extension
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.PluginRegistry import PluginRegistry
from UM.Application import Application
from UM.Version import Version
from UM.Message import Message
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
import json
import os
import tempfile
import platform
import zipfile
import shutil
from cura.CuraApplication import CuraApplication
i18n_catalog = i18nCatalog("cura")
class PluginBrowser(QObject, Extension):
def __init__(self, parent=None):
super().__init__(parent)
self._api_version = 2
self._api_version = 4
self._api_url = "http://software.ultimaker.com/cura/v%s/" % self._api_version
self._plugin_list_request = None
@ -34,11 +39,18 @@ class PluginBrowser(QObject, Extension):
self._download_plugin_reply = None
self._network_manager = None
self._plugin_registry = Application.getInstance().getPluginRegistry()
self._plugins_metadata = []
self._plugins_model = None
# Can be 'installed' or 'availble'
self._view = "available"
self._restart_required = False
self._dialog = None
self._restartDialog = None
self._download_progress = 0
self._is_downloading = False
@ -52,16 +64,29 @@ class PluginBrowser(QObject, Extension):
)
]
# Installed plugins are really installed after reboot. In order to prevent the user from downloading the
# same file over and over again, we keep track of the upgraded plugins.
# Installed plugins are really installed after reboot. In order to
# prevent the user from downloading the same file over and over again,
# we keep track of the upgraded plugins.
# NOTE: This will be depreciated in favor of the 'status' system.
self._newly_installed_plugin_ids = []
self._newly_uninstalled_plugin_ids = []
self._plugin_statuses = {} # type: Dict[str, str]
# variables for the license agreement dialog
self._license_dialog_plugin_name = ""
self._license_dialog_license_content = ""
self._license_dialog_plugin_file_location = ""
self._restart_dialog_message = ""
showLicenseDialog = pyqtSignal()
showRestartDialog = pyqtSignal()
pluginsMetadataChanged = pyqtSignal()
onDownloadProgressChanged = pyqtSignal()
onIsDownloadingChanged = pyqtSignal()
restartRequiredChanged = pyqtSignal()
viewChanged = pyqtSignal()
@pyqtSlot(result = str)
def getLicenseDialogPluginName(self):
@ -75,15 +100,19 @@ class PluginBrowser(QObject, Extension):
def getLicenseDialogLicenseContent(self):
return self._license_dialog_license_content
@pyqtSlot(result = str)
def getRestartDialogMessage(self):
return self._restart_dialog_message
def openLicenseDialog(self, plugin_name, license_content, plugin_file_location):
self._license_dialog_plugin_name = plugin_name
self._license_dialog_license_content = license_content
self._license_dialog_plugin_file_location = plugin_file_location
self.showLicenseDialog.emit()
pluginsMetadataChanged = pyqtSignal()
onDownloadProgressChanged = pyqtSignal()
onIsDownloadingChanged = pyqtSignal()
def openRestartDialog(self, message):
self._restart_dialog_message = message
self.showRestartDialog.emit()
@pyqtProperty(bool, notify = onIsDownloadingChanged)
def isDownloading(self):
@ -179,17 +208,46 @@ class PluginBrowser(QObject, Extension):
@pyqtSlot(str)
def installPlugin(self, file_path):
# Ensure that it starts with a /, as otherwise it doesn't work on windows.
if not file_path.startswith("/"):
location = "/" + file_path # Ensure that it starts with a /, as otherwise it doesn't work on windows.
location = "/" + file_path
else:
location = file_path
result = PluginRegistry.getInstance().installPlugin("file://" + location)
self._newly_installed_plugin_ids.append(result["id"])
self.pluginsMetadataChanged.emit()
self.openRestartDialog(result["message"])
self._restart_required = True
self.restartRequiredChanged.emit()
# Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"])
@pyqtSlot(str)
def removePlugin(self, plugin_id):
result = PluginRegistry.getInstance().uninstallPlugin(plugin_id)
self._newly_uninstalled_plugin_ids.append(result["id"])
self.pluginsMetadataChanged.emit()
self._restart_required = True
self.restartRequiredChanged.emit()
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Plugin browser"), result["message"])
@pyqtSlot(str)
def enablePlugin(self, plugin_id):
self._plugin_registry.enablePlugin(plugin_id)
self.pluginsMetadataChanged.emit()
Logger.log("i", "%s was set as 'active'", id)
@pyqtSlot(str)
def disablePlugin(self, plugin_id):
self._plugin_registry.disablePlugin(plugin_id)
self.pluginsMetadataChanged.emit()
Logger.log("i", "%s was set as 'deactive'", id)
@pyqtProperty(int, notify = onDownloadProgressChanged)
def downloadProgress(self):
return self._download_progress
@ -221,55 +279,70 @@ class PluginBrowser(QObject, Extension):
self.setDownloadProgress(0)
self.setIsDownloading(False)
@pyqtSlot(str)
def setView(self, view):
self._view = view
self.viewChanged.emit()
self.pluginsMetadataChanged.emit()
@pyqtProperty(QObject, notify=pluginsMetadataChanged)
def pluginsModel(self):
if self._plugins_model is None:
self._plugins_model = ListModel()
self._plugins_model.addRoleName(Qt.UserRole + 1, "name")
self._plugins_model.addRoleName(Qt.UserRole + 2, "version")
self._plugins_model.addRoleName(Qt.UserRole + 3, "short_description")
self._plugins_model.addRoleName(Qt.UserRole + 4, "author")
self._plugins_model.addRoleName(Qt.UserRole + 5, "already_installed")
self._plugins_model.addRoleName(Qt.UserRole + 6, "file_location")
self._plugins_model.addRoleName(Qt.UserRole + 7, "can_upgrade")
else:
self._plugins_model.clear()
items = []
for metadata in self._plugins_metadata:
items.append({
"name": metadata["label"],
"version": metadata["version"],
"short_description": metadata["short_description"],
"author": metadata["author"],
"already_installed": self._checkAlreadyInstalled(metadata["id"]),
"file_location": metadata["file_location"],
"can_upgrade": self._checkCanUpgrade(metadata["id"], metadata["version"])
})
self._plugins_model.setItems(items)
self._plugins_model = PluginsModel(None, self._view)
# self._plugins_model.update()
# Check each plugin the registry for matching plugin from server
# metadata, and if found, compare the versions. Higher version sets
# 'can_upgrade' to 'True':
for plugin in self._plugins_model.items:
if self._checkCanUpgrade(plugin["id"], plugin["version"]):
plugin["can_upgrade"] = True
for item in self._plugins_metadata:
if item["id"] == plugin["id"]:
plugin["update_url"] = item["file_location"]
return self._plugins_model
def _checkCanUpgrade(self, id, version):
plugin_registry = PluginRegistry.getInstance()
metadata = plugin_registry.getMetaData(id)
if metadata != {}:
if id in self._newly_installed_plugin_ids:
return False # We already updated this plugin.
current_version = Version(metadata["plugin"]["version"])
new_version = Version(version)
if new_version > current_version:
return True
# TODO: This could maybe be done more efficiently using a dictionary...
# Scan plugin server data for plugin with the given id:
for plugin in self._plugins_metadata:
if id == plugin["id"]:
reg_version = Version(version)
new_version = Version(plugin["version"])
if new_version > reg_version:
Logger.log("i", "%s has an update availible: %s", plugin["id"], plugin["version"])
return True
return False
def _checkAlreadyInstalled(self, id):
plugin_registry = PluginRegistry.getInstance()
metadata = plugin_registry.getMetaData(id)
if metadata != {}:
metadata = self._plugin_registry.getMetaData(id)
# We already installed this plugin, but the registry just doesn't know it yet.
if id in self._newly_installed_plugin_ids:
return True
# We already uninstalled this plugin, but the registry just doesn't know it yet:
elif id in self._newly_uninstalled_plugin_ids:
return False
elif metadata != {}:
return True
else:
if id in self._newly_installed_plugin_ids:
return True # We already installed this plugin, but the registry just doesn't know it yet.
return False
def _checkInstallStatus(self, plugin_id):
if plugin_id in self._plugin_registry.getInstalledPlugins():
return "installed"
else:
return "uninstalled"
def _checkEnabled(self, id):
if id in self._plugin_registry.getActivePlugins():
return True
return False
def _onRequestFinished(self, reply):
reply_url = reply.url().toString()
if reply.error() == QNetworkReply.TimeoutError:
@ -290,7 +363,10 @@ class PluginBrowser(QObject, Extension):
if reply_url == self._api_url + "plugins":
try:
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
# Add metadata to the manager:
self._plugins_metadata = json_data
self._plugin_registry.addExternalPlugins(self._plugins_metadata)
self.pluginsMetadataChanged.emit()
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
@ -316,3 +392,15 @@ class PluginBrowser(QObject, Extension):
self._network_manager = QNetworkAccessManager()
self._network_manager.finished.connect(self._onRequestFinished)
self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccesibleChanged)
@pyqtProperty(bool, notify=restartRequiredChanged)
def restartRequired(self):
return self._restart_required
@pyqtProperty(str, notify=viewChanged)
def viewing(self):
return self._view
@pyqtSlot()
def restart(self):
CuraApplication.getInstance().quit()

View file

@ -1,191 +1,209 @@
import UM 1.1 as UM
// Copyright (c) 2017 Ultimaker B.V.
// PluginBrowser is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
UM.Dialog
{
// TODO: Switch to QtQuick.Controls 2.x and remove QtQuick.Controls.Styles
import UM 1.1 as UM
Window {
id: base
title: catalog.i18nc("@title:window", "Find & Update plugins")
width: 600 * screenScaleFactor
height: 450 * screenScaleFactor
title: catalog.i18nc("@title:tab", "Plugins");
width: 800 * screenScaleFactor
height: 640 * screenScaleFactor
minimumWidth: 350 * screenScaleFactor
minimumHeight: 350 * screenScaleFactor
Item
{
anchors.fill: parent
Item
{
id: topBar
height: childrenRect.height;
width: parent.width
Label
{
id: introText
text: catalog.i18nc("@label", "Here you can find a list of Third Party plugins.")
width: parent.width
height: 30
}
color: UM.Theme.getColor("sidebar")
Button
{
id: refresh
text: catalog.i18nc("@action:button", "Refresh")
onClicked: manager.requestPluginList()
anchors.right: parent.right
enabled: !manager.isDownloading
Item {
id: view
anchors {
fill: parent
leftMargin: UM.Theme.getSize("default_margin").width
rightMargin: UM.Theme.getSize("default_margin").width
topMargin: UM.Theme.getSize("default_margin").height
bottomMargin: UM.Theme.getSize("default_margin").height
}
Rectangle {
id: topBar
width: parent.width
color: "transparent"
height: childrenRect.height
Row {
spacing: 12
height: childrenRect.height
width: childrenRect.width
anchors.horizontalCenter: parent.horizontalCenter
Button {
text: "Install"
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 96
implicitHeight: 48
Rectangle {
visible: manager.viewing == "available" ? true : false
color: UM.Theme.getColor("primary")
anchors.bottom: parent.bottom
width: parent.width
height: 3
}
}
label: Text {
text: control.text
color: UM.Theme.getColor("text")
font {
pixelSize: 15
}
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
onClicked: manager.setView("available")
}
Button {
text: "Manage"
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 96
implicitHeight: 48
Rectangle {
visible: manager.viewing == "installed" ? true : false
color: UM.Theme.getColor("primary")
anchors.bottom: parent.bottom
width: parent.width
height: 3
}
}
label: Text {
text: control.text
color: UM.Theme.getColor("text")
font {
pixelSize: 15
}
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
onClicked: manager.setView("installed")
}
}
}
ScrollView
{
// Scroll view breaks in QtQuick.Controls 2.x
ScrollView {
id: installedPluginList
width: parent.width
anchors.top: topBar.bottom
anchors.bottom: bottomBar.top
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
height: 400
anchors {
top: topBar.bottom
topMargin: UM.Theme.getSize("default_margin").height
bottom: bottomBar.top
bottomMargin: UM.Theme.getSize("default_margin").height
}
frameVisible: true
ListView
{
ListView {
id: pluginList
model: manager.pluginsModel
property var activePlugin
property var filter: "installed"
anchors.fill: parent
property var activePlugin
delegate: pluginDelegate
model: manager.pluginsModel
delegate: PluginEntry {}
}
}
Item
{
Rectangle {
id: bottomBar
width: parent.width
height: closeButton.height
height: childrenRect.height
color: "transparent"
anchors.bottom: parent.bottom
anchors.left: parent.left
ProgressBar
{
id: progressbar
anchors.bottom: parent.bottom
minimumValue: 0;
maximumValue: 100
anchors.left:parent.left
Label {
visible: manager.restartRequired
text: "You will need to restart Cura before changes in plugins have effect."
height: 30
verticalAlignment: Text.AlignVCenter
}
Button {
id: restartChangedButton
text: "Quit Cura"
anchors.right: closeButton.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
value: manager.isDownloading ? manager.downloadProgress : 0
visible: manager.restartRequired
iconName: "dialog-restart"
onClicked: manager.restart()
style: ButtonStyle {
background: Rectangle {
implicitWidth: 96
implicitHeight: 30
color: UM.Theme.getColor("primary")
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: UM.Theme.getColor("button_text")
font {
pixelSize: 13
bold: true
}
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
Button
{
Button {
id: closeButton
text: catalog.i18nc("@action:button", "Close")
iconName: "dialog-close"
onClicked:
{
if (manager.isDownloading)
{
onClicked: {
if ( manager.isDownloading ) {
manager.cancelDownload()
}
base.close();
}
anchors.bottom: parent.bottom
anchors.right: parent.right
}
}
Item
{
SystemPalette { id: palette }
Component
{
id: pluginDelegate
Rectangle
{
width: pluginList.width;
height: texts.height;
color: index % 2 ? palette.base : palette.alternateBase
Column
{
id: texts
width: parent.width
height: childrenRect.height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: downloadButton.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
Label
{
text: "<b>" + model.name + "</b>" + ((model.author !== "") ? (" - " + model.author) : "")
width: contentWidth
height: contentHeight + UM.Theme.getSize("default_margin").height
verticalAlignment: Text.AlignVCenter
}
Label
{
text: model.short_description
width: parent.width
height: contentHeight + UM.Theme.getSize("default_margin").height
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 96
implicitHeight: 30
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
Button
{
id: downloadButton
text:
{
if (manager.isDownloading && pluginList.activePlugin == model)
{
return catalog.i18nc("@action:button", "Cancel");
}
else if (model.already_installed)
{
if (model.can_upgrade)
{
return catalog.i18nc("@action:button", "Upgrade");
}
return catalog.i18nc("@action:button", "Installed");
}
return catalog.i18nc("@action:button", "Download");
}
onClicked:
{
if(!manager.isDownloading)
{
pluginList.activePlugin = model;
manager.downloadAndInstallPlugin(model.file_location);
}
else
{
manager.cancelDownload();
}
}
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
enabled:
{
if (manager.isDownloading)
{
return (pluginList.activePlugin == model);
}
else
{
return (!model.already_installed || model.can_upgrade);
}
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: UM.Theme.getColor("text")
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
}
UM.I18nCatalog { id: catalog; name: "cura" }
Connections
{
Connections {
target: manager
onShowLicenseDialog:
{
onShowLicenseDialog: {
licenseDialog.pluginName = manager.getLicenseDialogPluginName();
licenseDialog.licenseContent = manager.getLicenseDialogLicenseContent();
licenseDialog.pluginFileLocation = manager.getLicenseDialogPluginFileLocation();
@ -193,8 +211,7 @@ UM.Dialog
}
}
UM.Dialog
{
UM.Dialog {
id: licenseDialog
title: catalog.i18nc("@title:window", "Plugin License Agreement")
@ -258,5 +275,94 @@ UM.Dialog
}
]
}
Connections {
target: manager
onShowRestartDialog: {
restartDialog.message = manager.getRestartDialogMessage();
restartDialog.show();
}
}
Window {
id: restartDialog
// title: catalog.i18nc("@title:tab", "Plugins");
width: 360 * screenScaleFactor
height: 120 * screenScaleFactor
minimumWidth: 360 * screenScaleFactor
minimumHeight: 120 * screenScaleFactor
color: UM.Theme.getColor("sidebar")
property var message;
Text {
id: message
anchors {
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height
}
text: restartDialog.message != null ? restartDialog.message : ""
}
Button {
id: laterButton
text: "Later"
onClicked: restartDialog.close();
anchors {
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
bottom: parent.bottom
bottomMargin: UM.Theme.getSize("default_margin").height
}
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 96
implicitHeight: 30
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: UM.Theme.getColor("text")
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
Button {
id: restartButton
text: "Quit Cura"
anchors {
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
bottom: parent.bottom
bottomMargin: UM.Theme.getSize("default_margin").height
}
onClicked: manager.restart()
style: ButtonStyle {
background: Rectangle {
implicitWidth: 96
implicitHeight: 30
color: UM.Theme.getColor("primary")
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: UM.Theme.getColor("button_text")
font {
pixelSize: 13
bold: true
}
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
}
}
}

View file

@ -0,0 +1,474 @@
// Copyright (c) 2017 Ultimaker B.V.
// PluginBrowser is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
// TODO: Switch to QtQuick.Controls 2.x and remove QtQuick.Controls.Styles
import UM 1.1 as UM
Component {
id: pluginDelegate
Rectangle {
// Don't show required plugins as they can't be managed anyway:
height: !model.required ? 84 : 0
visible: !model.required ? true : false
color: "transparent"
anchors {
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
}
// Bottom border:
Rectangle {
color: UM.Theme.getColor("lining")
width: parent.width
height: 1
anchors.bottom: parent.bottom
}
// Plugin info
Column {
id: pluginInfo
property var color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining")
// Styling:
height: parent.height
anchors {
left: parent.left
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height
right: authorInfo.left
rightMargin: UM.Theme.getSize("default_margin").width
}
Label {
text: model.name
width: parent.width
height: 24
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
font {
pixelSize: 13
bold: true
}
color: pluginInfo.color
}
Text {
text: model.description
width: parent.width
height: 36
clip: true
wrapMode: Text.WordWrap
color: pluginInfo.color
elide: Text.ElideRight
}
}
// Author info
Column {
id: authorInfo
width: 192
height: parent.height
anchors {
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height
right: pluginActions.left
rightMargin: UM.Theme.getSize("default_margin").width
}
Label {
text: "<a href=\"mailto:"+model.author_email+"?Subject=Cura: "+model.name+"\">"+model.author+"</a>"
width: parent.width
height: 24
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
onLinkActivated: Qt.openUrlExternally("mailto:"+model.author_email+"?Subject=Cura: "+model.name+" Plugin")
color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining")
}
}
// Plugin actions
Row {
id: pluginActions
width: 96
height: parent.height
anchors {
top: parent.top
right: parent.right
topMargin: UM.Theme.getSize("default_margin").height
}
layoutDirection: Qt.RightToLeft
spacing: UM.Theme.getSize("default_margin").width
// For 3rd-Party Plugins:
Button {
id: installButton
text: {
if ( manager.isDownloading && pluginList.activePlugin == model ) {
return catalog.i18nc( "@action:button", "Cancel" );
} else {
if (model.can_upgrade) {
return catalog.i18nc("@action:button", "Update");
}
return catalog.i18nc("@action:button", "Install");
}
}
visible: model.external && ((model.status !== "installed") || model.can_upgrade)
style: ButtonStyle {
background: Rectangle {
implicitWidth: 96
implicitHeight: 30
color: "transparent"
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Label {
text: control.text
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
onClicked: {
if ( manager.isDownloading && pluginList.activePlugin == model ) {
manager.cancelDownload();
} else {
pluginList.activePlugin = model;
if ( model.can_upgrade ) {
manager.downloadAndInstallPlugin( model.update_url );
} else {
manager.downloadAndInstallPlugin( model.file_location );
}
}
}
}
Button {
id: removeButton
text: "Uninstall"
visible: model.can_uninstall && model.status == "installed"
enabled: !manager.isDownloading
style: ButtonStyle {
background: Rectangle {
implicitWidth: 96
implicitHeight: 30
color: "transparent"
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
text: control.text
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
onClicked: manager.removePlugin( model.id )
}
// For Ultimaker Plugins:
Button {
id: enableButton
text: "Enable"
visible: !model.external && model.enabled == false
style: ButtonStyle {
background: Rectangle {
implicitWidth: 96
implicitHeight: 30
color: "transparent"
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
text: control.text
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
onClicked: {
manager.enablePlugin(model.id);
}
}
Button {
id: disableButton
text: "Disable"
visible: !model.external && model.enabled == true
style: ButtonStyle {
background: Rectangle {
implicitWidth: 96
implicitHeight: 30
color: "transparent"
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
text: control.text
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}
onClicked: {
manager.disablePlugin(model.id);
}
}
/*
Rectangle {
id: removeControls
visible: model.status == "installed" && model.enabled
width: 96
height: 30
color: "transparent"
Button {
id: removeButton
text: "Disable"
enabled: {
if ( manager.isDownloading && pluginList.activePlugin == model ) {
return false;
} else if ( model.required ) {
return false;
} else {
return true;
}
}
onClicked: {
manager.disablePlugin(model.id);
}
style: ButtonStyle {
background: Rectangle {
color: "white"
implicitWidth: 96
implicitHeight: 30
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: "grey"
text: control.text
horizontalAlignment: Text.AlignLeft
}
}
}
Button {
id: removeDropDown
property bool open: false
UM.RecolorImage {
anchors.centerIn: parent
height: 10
width: 10
source: UM.Theme.getIcon("arrow_bottom")
color: "grey"
}
enabled: {
if ( manager.isDownloading && pluginList.activePlugin == model ) {
return false;
} else if ( model.required ) {
return false;
} else {
return true;
}
}
anchors.right: parent.right
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 30
implicitHeight: 30
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: "grey"
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
// For the disable option:
// onClicked: pluginList.model.setEnabled(model.id, checked)
onClicked: {
if ( !removeDropDown.open ) {
removeDropDown.open = true
}
else {
removeDropDown.open = false
}
}
}
Rectangle {
id: divider
width: 1
height: parent.height
anchors.right: removeDropDown.left
color: UM.Theme.getColor("lining")
}
Column {
id: options
anchors {
top: removeButton.bottom
left: parent.left
right: parent.right
}
height: childrenRect.height
visible: removeDropDown.open
Button {
id: disableButton
text: "Remove"
height: 30
width: parent.width
onClicked: {
removeDropDown.open = false;
manager.removePlugin( model.id );
}
}
}
}
*/
/*
Button {
id: enableButton
visible: !model.enabled && model.status == "installed"
onClicked: manager.enablePlugin( model.id );
text: "Enable"
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 96
implicitHeight: 30
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: UM.Theme.getColor("text")
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
Button {
id: updateButton
visible: model.status == "installed" && model.can_upgrade && model.enabled
// visible: model.already_installed
text: {
// If currently downloading:
if ( manager.isDownloading && pluginList.activePlugin == model ) {
return catalog.i18nc( "@action:button", "Cancel" );
} else {
return catalog.i18nc("@action:button", "Update");
}
}
style: ButtonStyle {
background: Rectangle {
color: UM.Theme.getColor("primary")
implicitWidth: 96
implicitHeight: 30
// radius: 4
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: "white"
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
Button {
id: externalControls
visible: model.status == "available" ? true : false
text: {
// If currently downloading:
if ( manager.isDownloading && pluginList.activePlugin == model ) {
return catalog.i18nc( "@action:button", "Cancel" );
} else {
return catalog.i18nc("@action:button", "Install");
}
}
onClicked: {
if ( manager.isDownloading && pluginList.activePlugin == model ) {
manager.cancelDownload();
} else {
pluginList.activePlugin = model;
manager.downloadAndInstallPlugin( model.file_location );
}
}
style: ButtonStyle {
background: Rectangle {
color: "transparent"
implicitWidth: 96
implicitHeight: 30
border {
width: 1
color: UM.Theme.getColor("lining")
}
}
label: Text {
verticalAlignment: Text.AlignVCenter
color: "grey"
text: control.text
horizontalAlignment: Text.AlignHCenter
}
}
}
*/
ProgressBar {
id: progressbar
minimumValue: 0;
maximumValue: 100
anchors.left: installButton.left
anchors.right: installButton.right
anchors.top: installButton.bottom
anchors.topMargin: 4
value: manager.isDownloading ? manager.downloadProgress : 0
visible: manager.isDownloading && pluginList.activePlugin == model
style: ProgressBarStyle {
background: Rectangle {
color: "lightgray"
implicitHeight: 6
}
progress: Rectangle {
color: UM.Theme.getColor("primary")
}
}
}
}
}
}

View file

@ -0,0 +1,209 @@
# 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()
# If the scene does not have a gcode, do nothing
if not hasattr(scene, "gcode_dict"):
return
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().getMultiBuildPlateModel().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:
try:
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)
except Exception as e:
Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e)))
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)]:
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)
# 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.round((base.width / 2) - UM.Theme.getSize("default_margin").width)
property int textMargin: Math.round(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.round(control.width / 2.7)
height: Math.round(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.round(control.width / 2.5)
height: Math.round(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.round(control.width / 2.5)
height: Math.round(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.round(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.round(parent.width / 2)
height: Math.round(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,163 @@
# 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
## Convenience function to produce a line of g-code.
#
# You can put in an original g-code line and it'll re-use all the values
# in that line.
# All other keyword parameters are put in the result in g-code's format.
# For instance, if you put ``G=1`` in the parameters, it will output
# ``G1``. If you put ``G=1, X=100`` in the parameters, it will output
# ``G1 X100``. The parameters G and M will always be put first. The
# parameters T and S will be put second (or first if there is no G or M).
# The rest of the parameters will be put in arbitrary order.
# \param line The original g-code line that must be modified. If not
# provided, an entirely new g-code line will be produced.
# \return A line of g-code with the desired parameters filled in.
def putValue(self, line = "", **kwargs):
#Strip the comment.
comment = ""
if ";" in line:
comment = line[line.find(";"):]
line = line[:line.find(";")] #Strip the comment.
#Parse the original g-code line.
for part in line.split(" "):
if part == "":
continue
parameter = part[0]
if parameter in kwargs:
continue #Skip this one. The user-provided parameter overwrites the one in the line.
value = part[1:]
kwargs[parameter] = value
#Write the new g-code line.
result = ""
priority_parameters = ["G", "M", "T", "S", "F", "X", "Y", "Z", "E"] #First some parameters that get priority. In order of priority!
for priority_key in priority_parameters:
if priority_key in kwargs:
if result != "":
result += " "
result += priority_key + str(kwargs[priority_key])
del kwargs[priority_key]
for key, value in kwargs.items():
if result != "":
result += " "
result += key + str(value)
#Put the comment back in.
if comment != "":
if result != "":
result += " "
result += ";" + comment
return result
## 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,495 @@
# ChangeAtZ 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 ChangeAtZ 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 ChangeAtZ(Script):
version = "5.1.1"
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"ChangeAtZ """ + self.version + """ (Experimental)",
"key":"ChangeAtZ",
"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": "Change Height",
"description": "Z height to change 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": "Change Layer",
"description": "Layer no. to change 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: Change value and keep it for the rest, Change 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 change",
"unit": "",
"type": "int",
"default_value": 1,
"minimum_value": "1",
"maximum_value_warning": "50",
"enabled": "c_behavior == 'keep_value'"
},
"e1_Change_speed":
{
"label": "Change Speed",
"description": "Select if total speed (print and travel) has to be cahnged",
"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_Change_speed"
},
"f1_Change_printspeed":
{
"label": "Change Print Speed",
"description": "Select if print speed has to be changed",
"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_Change_printspeed"
},
"g1_Change_flowrate":
{
"label": "Change Flow Rate",
"description": "Select if flow rate has to be changed",
"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_Change_flowrate"
},
"g3_Change_flowrateOne":
{
"label": "Change Flow Rate 1",
"description": "Select if first extruder flow rate has to be changed",
"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_Change_flowrateOne"
},
"g5_Change_flowrateTwo":
{
"label": "Change Flow Rate 2",
"description": "Select if second extruder flow rate has to be changed",
"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_Change_flowrateTwo"
},
"h1_Change_bedTemp":
{
"label": "Change Bed Temp",
"description": "Select if Bed Temperature has to be changed",
"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_Change_bedTemp"
},
"i1_Change_extruderOne":
{
"label": "Change Extruder 1 Temp",
"description": "Select if First Extruder Temperature has to be changed",
"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_Change_extruderOne"
},
"i3_Change_extruderTwo":
{
"label": "Change Extruder 2 Temp",
"description": "Select if Second Extruder Temperature has to be changed",
"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_Change_extruderTwo"
},
"j1_Change_fanSpeed":
{
"label": "Change Fan Speed",
"description": "Select if Fan Speed has to be changed",
"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_Change_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 ";ChangeAtZ" in key and not ";LAYER:" in key):
return default
subPart = line[line.find(key) + len(key):] #allows for string lengths larger than 1
if ";ChangeAtZ" 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 changes should apply
ChangeProp = {"speed": self.getSettingValueByKey("e1_Change_speed"),
"flowrate": self.getSettingValueByKey("g1_Change_flowrate"),
"flowrateOne": self.getSettingValueByKey("g3_Change_flowrateOne"),
"flowrateTwo": self.getSettingValueByKey("g5_Change_flowrateTwo"),
"bedTemp": self.getSettingValueByKey("h1_Change_bedTemp"),
"extruderOne": self.getSettingValueByKey("i1_Change_extruderOne"),
"extruderTwo": self.getSettingValueByKey("i3_Change_extruderTwo"),
"fanSpeed": self.getSettingValueByKey("j1_Change_fanSpeed")}
ChangePrintSpeed = self.getSettingValueByKey("f1_Change_printspeed")
ChangeStrings = {"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 += ";ChangeAtZ instances: %d\n" % TWinstances
if not ("M84" in line or "M25" in line or ("G1" in line and ChangePrintSpeed and (state==3 or state==4)) or
";ChangeAtZ instances:" in line):
modified_gcode += line + "\n"
IsUM2 = ("FLAVOR:UltiGCode" in line) or IsUM2 #Flavor is UltiGCode!
if ";ChangeAtZ-state" in line: #checks for state change comment
state = self.getValue(line, ";ChangeAtZ-state", state)
if ";ChangeAtZ 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 change 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 ChangeProp["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 ChangePrintSpeed 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 changing 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 ChangeProp:
if ChangeProp[key] and old[key]==-1: #old value is not known
oldValueUnknown = True
if oldValueUnknown: #the changing 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 changing\n" % (TWinstances-1)
if behavior == 1: #single layer change only and then reset
twLayers = 1
if ChangePrintSpeed 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 += ";ChangeAtZ V%s: executed at Layer %d\n" % (self.version,layer)
modified_gcode += "M117 Printing... ch@L%4d\n" % layer
else:
modified_gcode += (";ChangeAtZ V%s: executed at %1.2f mm\n" % (self.version,z))
modified_gcode += "M117 Printing... ch@%5.1f\n" % z
for key in ChangeProp:
if ChangeProp[key]:
modified_gcode += ChangeStrings[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 += ";ChangeAtZ V%s: reset on Layer %d\n" % (self.version,layer)
else:
modified_gcode += ";ChangeAtZ 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 ChangeProp:
if ChangeProp[key]:
modified_gcode += ChangeStrings[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 change level or at level 0
state = 2
done_layers = 0
if targetL_i > -100000:
modified_gcode += ";ChangeAtZ V%s: reset below Layer %d\n" % (self.version,targetL_i)
else:
modified_gcode += ";ChangeAtZ 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 ChangeProp:
if ChangeProp[key]:
modified_gcode += ChangeStrings[key] % float(old[key])
data[index] = modified_gcode
index += 1
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,275 @@
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_at":
{
"label": "Pause at",
"description": "Whether to pause at a certain height or at a certain layer.",
"type": "enum",
"options": {"height": "Height", "layer_no": "Layer No."},
"default_value": "height"
},
"pause_height":
{
"label": "Pause Height",
"description": "At what height should the pause occur",
"unit": "mm",
"type": "float",
"default_value": 5.0,
"minimum_value": "0",
"minimum_value_warning": "0.27",
"enabled": "pause_at == 'height'"
},
"pause_layer":
{
"label": "Pause Layer",
"description": "At what layer should the pause occur",
"type": "int",
"value": "math.floor((pause_height - 0.27) / 0.1) + 1",
"minimum_value": "0",
"minimum_value_warning": "1",
"enabled": "pause_at == 'layer_no'"
},
"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.
pause_at = self.getSettingValueByKey("pause_at")
pause_height = self.getSettingValueByKey("pause_height")
pause_layer = self.getSettingValueByKey("pause_layer")
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.
current_z = 0
got_first_g_cmd_on_layer_0 = False
for index, layer in enumerate(data):
lines = layer.split("\n")
for line in lines:
if ";LAYER:0" in line:
layers_started = True
if not layers_started:
continue
if self.getValue(line, "Z") is not None:
current_z = self.getValue(line, "Z")
if pause_at == "height":
if self.getValue(line, "G") != 1 and self.getValue(line, "G") != 0:
continue
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)
current_height = current_z - layer_0_z
if current_height < pause_height:
break #Try the next layer.
else: #Pause at layer.
if not line.startswith(";LAYER:"):
continue
current_layer = line[len(";LAYER:"):]
try:
current_layer = int(current_layer)
except ValueError: #Couldn't cast to int. Something is wrong with this g-code data.
continue
if current_layer < pause_layer:
break #Try the next layer.
prevLayer = data[index - 1]
prevLines = prevLayer.split("\n")
current_e = 0.
# Access last layer, browse it backwards to find
# last extruder absolute position
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
# Get extruder's absolute position at the
# begining of the first layer redone
# see https://github.com/nallath/PostProcessingPlugin/issues/55
if i == redo_layers:
prevLines = prevLayer.split("\n")
for line in prevLines:
new_e = self.getValue(line, 'E', current_e)
if new_e != current_e:
current_e = new_e
break
prepend_gcode = ";TYPE:CUSTOM\n"
prepend_gcode += ";added code by post processing\n"
prepend_gcode += ";script: PauseAtHeight.py\n"
if pause_at == "height":
prepend_gcode += ";current z: {z}\n".format(z = current_z)
prepend_gcode += ";current height: {height}\n".format(height = current_height)
else:
prepend_gcode += ";current layer: {layer}\n".format(layer = current_layer)
# Retraction
prepend_gcode += self.putValue(M = 83) + "\n"
if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
# Move the head away
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n"
prepend_gcode += self.putValue(G = 1, X = park_x, Y = park_y, F = 9000) + "\n"
if current_z < 15:
prepend_gcode += self.putValue(G = 1, Z = 15, F = 300) + "\n"
# Disable the E steppers
prepend_gcode += self.putValue(M = 84, E = 0) + "\n"
# Set extruder standby temperature
prepend_gcode += self.putValue(M = 104, S = standby_temperature) + "; standby temperature\n"
# Wait till the user continues printing
prepend_gcode += self.putValue(M = 0) + ";Do the actual pause\n"
# Set extruder resume temperature
prepend_gcode += self.putValue(M = 109, S = resume_temperature) + "; resume temperature\n"
# Push the filament back,
if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n"
# Optionally extrude material
if extrude_amount != 0:
prepend_gcode += self.putValue(G = 1, E = extrude_amount, F = extrude_speed * 60) + "\n"
# and retract again, the properly primes the nozzle
# when changing filament.
if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
# Move the head back
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n"
prepend_gcode += self.putValue(G = 1, X = x, Y = y, F = 9000) + "\n"
if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n"
prepend_gcode += self.putValue(G = 1, F = 9000) + "\n"
prepend_gcode += self.putValue(M = 82) + "\n"
# reset extrude value to pre pause value
prepend_gcode += self.putValue(G = 92, E = current_e) + "\n"
layer = prepend_gcode + layer
# Override the data of this layer with the
# modified data
data[index] = layer
return data
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

@ -93,6 +93,7 @@ class SimulationPass(RenderPass):
self.bind()
tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay, backface_cull = True)
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
head_position = None # Indicates the current position of the print head
nozzle_node = None

View file

@ -49,7 +49,7 @@ UM.PointingRectangle {
anchors {
left: parent.left
leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
leftMargin: Math.round(UM.Theme.getSize("default_margin").width / 2)
verticalCenter: parent.verticalCenter
}
@ -91,7 +91,7 @@ UM.PointingRectangle {
anchors {
left: parent.right
leftMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
leftMargin: Math.round(UM.Theme.getSize("default_margin").width / 2)
verticalCenter: parent.verticalCenter
}

View file

@ -1,9 +1,10 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QOpenGLContext
from PyQt5.QtWidgets import QApplication
from UM.Application import Application
@ -13,6 +14,7 @@ from UM.Logger import Logger
from UM.Math.Color import Color
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Message import Message
from UM.Platform import Platform
from UM.PluginRegistry import PluginRegistry
from UM.Preferences import Preferences
from UM.Resources import Resources
@ -23,7 +25,8 @@ from UM.View.GL.OpenGL import OpenGL
from UM.View.GL.OpenGLContext import OpenGLContext
from UM.View.View import View
from UM.i18n import i18nCatalog
from cura.ConvexHullNode import ConvexHullNode
from cura.Scene.ConvexHullNode import ConvexHullNode
from cura.CuraApplication import CuraApplication
from .NozzleNode import NozzleNode
from .SimulationPass import SimulationPass
@ -95,13 +98,16 @@ class SimulationView(View):
self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
self._compatibility_mode = True # for safety
self._compatibility_mode = self._evaluateCompatibilityMode()
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"),
title = catalog.i18nc("@info:title", "Simulation View"))
def _evaluateCompatibilityMode(self):
return OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
def _resetSettings(self):
self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed
self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
self._extruder_count = 0
self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
self._show_travel_moves = 0
@ -124,7 +130,7 @@ class SimulationView(View):
# Currently the RenderPass constructor requires a size > 0
# This should be fixed in RenderPass's constructor.
self._layer_pass = SimulationPass(1, 1)
self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
self._compatibility_mode = self._evaluateCompatibilityMode()
self._layer_pass.setSimulationView(self)
return self._layer_pass
@ -336,12 +342,22 @@ class SimulationView(View):
min_layer_number = sys.maxsize
max_layer_number = -sys.maxsize
for layer_id in layer_data.getLayers():
# If a layer doesn't contain any polygons, skip it (for infill meshes taller than print objects
if len(layer_data.getLayer(layer_id).polygons) < 1:
continue
# Store the max and min feedrates and thicknesses for display purposes
for p in layer_data.getLayer(layer_id).polygons:
self._max_feedrate = max(float(p.lineFeedrates.max()), self._max_feedrate)
self._min_feedrate = min(float(p.lineFeedrates.min()), self._min_feedrate)
self._max_thickness = max(float(p.lineThicknesses.max()), self._max_thickness)
self._min_thickness = min(float(p.lineThicknesses.min()), self._min_thickness)
try:
self._min_thickness = min(float(p.lineThicknesses[numpy.nonzero(p.lineThicknesses)].min()), self._min_thickness)
except:
# Sometimes, when importing a GCode the line thicknesses are zero and so the minimum (avoiding
# the zero) can't be calculated
Logger.log("i", "Min thickness can't be calculated because all the values are zero")
if max_layer_number < layer_id:
max_layer_number = layer_id
if min_layer_number > layer_id:
@ -414,6 +430,23 @@ class SimulationView(View):
return True
if event.type == Event.ViewActivateEvent:
# FIX: on Max OS X, somehow QOpenGLContext.currentContext() can become None during View switching.
# This can happen when you do the following steps:
# 1. Start Cura
# 2. Load a model
# 3. Switch to Custom mode
# 4. Select the model and click on the per-object tool icon
# 5. Switch view to Layer view or X-Ray
# 6. Cura will very likely crash
# It seems to be a timing issue that the currentContext can somehow be empty, but I have no clue why.
# This fix tries to reschedule the view changing event call on the Qt thread again if the current OpenGL
# context is None.
if Platform.isOSX():
if QOpenGLContext.currentContext() is None:
Logger.log("d", "current context of OpenGL is empty on Mac OS X, will try to create shaders later")
CuraApplication.getInstance().callLater(lambda e=event: self.event(e))
return
# Make sure the SimulationPass is created
layer_pass = self.getSimulationPass()
self.getRenderer().addRenderPass(layer_pass)
@ -509,8 +542,7 @@ class SimulationView(View):
def _updateWithPreferences(self):
self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(
Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
self._compatibility_mode = self._evaluateCompatibilityMode()
self.setSimulationViewType(int(float(Preferences.getInstance().getValue("layerview/layer_view_type"))));
@ -607,4 +639,3 @@ class _CreateTopLayersJob(Job):
def cancel(self):
self._cancel = True
super().cancel()

View file

@ -61,7 +61,7 @@ Item
Button {
id: collapseButton
anchors.top: parent.top
anchors.topMargin: Math.floor(UM.Theme.getSize("default_margin").height + (UM.Theme.getSize("layerview_row").height - UM.Theme.getSize("default_margin").height) / 2)
anchors.topMargin: Math.round(UM.Theme.getSize("default_margin").height + (UM.Theme.getSize("layerview_row").height - UM.Theme.getSize("default_margin").height) / 2)
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
@ -176,7 +176,6 @@ Item
viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2);
viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3);
}
}
Label
@ -194,7 +193,7 @@ Item
Item
{
height: Math.floor(UM.Theme.getSize("default_margin").width / 2)
height: Math.round(UM.Theme.getSize("default_margin").width / 2)
width: width
}
@ -232,7 +231,7 @@ Item
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: model.color
radius: width / 2
radius: Math.round(width / 2)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: !viewSettings.show_legend & !viewSettings.show_gradient
@ -250,7 +249,7 @@ Item
anchors.verticalCenter: parent.verticalCenter
anchors.left: extrudersModelCheckBox.left;
anchors.right: extrudersModelCheckBox.right;
anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
anchors.leftMargin: UM.Theme.getSize("checkbox").width + Math.round(UM.Theme.getSize("default_margin").width/2)
anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
}
}
@ -317,7 +316,7 @@ Item
anchors.verticalCenter: parent.verticalCenter
anchors.left: legendModelCheckBox.left;
anchors.right: legendModelCheckBox.right;
anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
anchors.leftMargin: UM.Theme.getSize("checkbox").width + Math.round(UM.Theme.getSize("default_margin").width/2)
anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
}
}
@ -462,7 +461,7 @@ Item
visible: viewSettings.show_feedrate_gradient
anchors.left: parent.right
height: parent.width
width: UM.Theme.getSize("layerview_row").height * 1.5
width: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
@ -486,37 +485,37 @@ Item
}
}
// Gradient colors for layer thickness
// Gradient colors for layer thickness (similar to parula colormap)
Rectangle { // In QML 5.9 can be changed by LinearGradient
// Invert values because then the bar is rotated 90 degrees
id: thicknessGradient
visible: viewSettings.show_thickness_gradient
anchors.left: parent.right
height: parent.width
width: UM.Theme.getSize("layerview_row").height * 1.5
width: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
gradient: Gradient {
GradientStop {
position: 0.000
color: Qt.rgba(1, 0, 0, 1)
color: Qt.rgba(1, 1, 0, 1)
}
GradientStop {
position: 0.25
color: Qt.rgba(0.5, 0.5, 0, 1)
color: Qt.rgba(1, 0.75, 0.25, 1)
}
GradientStop {
position: 0.5
color: Qt.rgba(0, 1, 0, 1)
color: Qt.rgba(0, 0.75, 0.5, 1)
}
GradientStop {
position: 0.75
color: Qt.rgba(0, 0.5, 0.5, 1)
color: Qt.rgba(0, 0.375, 0.75, 1)
}
GradientStop {
position: 1.0
color: Qt.rgba(0, 0, 1, 1)
color: Qt.rgba(0, 0, 0.5, 1)
}
}
}

View file

@ -1,4 +1,4 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
@ -117,7 +117,7 @@ class SimulationViewProxy(QObject):
def setSimulationViewType(self, layer_view_type):
active_view = self._controller.getActiveView()
if isinstance(active_view, SimulationView.SimulationView.SimulationView):
active_view.setSimulationViewisinstance(layer_view_type)
active_view.setSimulationViewType(layer_view_type)
@pyqtSlot(result=int)
def getSimulationViewType(self):

View file

@ -1,4 +1,4 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtQml import qmlRegisterSingletonType
@ -18,7 +18,7 @@ def getMetaData():
}
def createSimulationViewProxy(engine, script_engine):
return SimulationViewProxy.SimulatorViewProxy()
return SimulationViewProxy.SimulationViewProxy()
def register(app):
simulation_view = SimulationView.SimulationView()

View file

@ -54,9 +54,13 @@ vertex41core =
vec4 layerThicknessGradientColor(float abs_value, float min_value, float max_value)
{
float value = (abs_value - min_value)/(max_value - min_value);
float red = max(2*value-1, 0);
float green = 1-abs(1-2*value);
float blue = max(1-2*value, 0);
float red = min(max(4*value-2, 0), 1);
float green = min(1.5*value, 0.75);
if (value > 0.75)
{
green = value;
}
float blue = 0.75-abs(0.25-value);
return vec4(red, green, blue, 1.0);
}
@ -187,7 +191,7 @@ geometry41core =
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert));
//And reverse so that the line is also visible from the back side.
//And reverse so that the line is also visible from the back side.
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
@ -212,17 +216,17 @@ geometry41core =
EndPrimitive();
// left side
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
EndPrimitive();
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
EndPrimitive();

View file

@ -145,35 +145,42 @@ geometry41core =
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz_head + g_vertex_offset_vert));
//And reverse so that the line is also visible from the back side.
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz + g_vertex_offset_vert));
EndPrimitive();
} else {
// All normal lines are rendered as 3d tubes.
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[1], v_color[1], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[1].gl_Position - g_vertex_offset_horz));
EndPrimitive();
// left side
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
EndPrimitive();
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_vert, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_vert));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz_head, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz_head));
myEmitVertex(v_vertex[0], v_color[0], g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position + g_vertex_offset_horz));
myEmitVertex(v_vertex[0], v_color[0], -g_vertex_normal_horz, u_viewProjectionMatrix * (gl_in[0].gl_Position - g_vertex_offset_horz));
EndPrimitive();

View file

@ -39,19 +39,26 @@ class SliceInfo(Extension):
Preferences.getInstance().addPreference("info/send_slice_info", True)
Preferences.getInstance().addPreference("info/asked_send_slice_info", False)
if not Preferences.getInstance().getValue("info/asked_send_slice_info") and Preferences.getInstance().getValue("info/send_slice_info"):
self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymised slicing statistics. You can disable this in the preferences."),
if not Preferences.getInstance().getValue("info/asked_send_slice_info"):
self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymized usage statistics."),
lifetime = 0,
dismissable = False,
title = catalog.i18nc("@info:title", "Collecting Data"))
self.send_slice_info_message.addAction("Dismiss", catalog.i18nc("@action:button", "Dismiss"), None, "")
self.send_slice_info_message.addAction("Dismiss", name = catalog.i18nc("@action:button", "Allow"), icon = None,
description = catalog.i18nc("@action:tooltip", "Allow Cura to send anonymized usage statistics to help prioritize future improvements to Cura. Some of your preferences and settings are sent, the Cura version and a hash of the models you're slicing."))
self.send_slice_info_message.addAction("Disable", name = catalog.i18nc("@action:button", "Disable"), icon = None,
description = catalog.i18nc("@action:tooltip", "Don't allow Cura to send anonymized usage statistics. You can enable it again in the preferences."), button_style = Message.ActionButtonStyle.LINK)
self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
self.send_slice_info_message.show()
## Perform action based on user input.
# Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it.
def messageActionTriggered(self, message_id, action_id):
self.send_slice_info_message.hide()
Preferences.getInstance().setValue("info/asked_send_slice_info", True)
if action_id == "Disable":
CuraApplication.getInstance().showPreferences()
self.send_slice_info_message.hide()
def _onWriteStarted(self, output_device):
try:
@ -100,7 +107,7 @@ class SliceInfo(Extension):
"brand": extruder.material.getMetaData().get("brand", "")
}
extruder_position = int(extruder.getMetaDataEntry("position", "0"))
if extruder_position in print_information.materialLengths:
if len(print_information.materialLengths) > extruder_position:
extruder_dict["material_used"] = print_information.materialLengths[extruder_position]
extruder_dict["variant"] = extruder.variant.getName()
extruder_dict["nozzle_size"] = extruder.getProperty("machine_nozzle_size", "value")
@ -159,7 +166,7 @@ class SliceInfo(Extension):
data["models"].append(model)
print_times = print_information._print_time_message_values
print_times = print_information.printTimes()
data["print_times"] = {"travel": int(print_times["travel"].getDisplayString(DurationFormat.Format.Seconds)),
"support": int(print_times["support"].getDisplayString(DurationFormat.Format.Seconds)),
"infill": int(print_times["infill"].getDisplayString(DurationFormat.Format.Seconds)),

View file

@ -28,6 +28,7 @@ class SolidView(View):
self._enabled_shader = None
self._disabled_shader = None
self._non_printing_shader = None
self._support_mesh_shader = None
self._extruders_model = ExtrudersModel()
self._theme = None
@ -54,6 +55,11 @@ class SolidView(View):
self._non_printing_shader.setUniformValue("u_diffuseColor", Color(*self._theme.getColor("model_non_printing").getRgb()))
self._non_printing_shader.setUniformValue("u_opacity", 0.6)
if not self._support_mesh_shader:
self._support_mesh_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader"))
self._support_mesh_shader.setUniformValue("u_vertical_stripes", True)
self._support_mesh_shader.setUniformValue("u_width", 5.0)
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
@ -110,13 +116,23 @@ class SolidView(View):
except ValueError:
pass
if getattr(node, "_non_printing_mesh", False):
if node.callDecoration("isNonPrintingMesh"):
if per_mesh_stack and (per_mesh_stack.getProperty("infill_mesh", "value") or per_mesh_stack.getProperty("cutting_mesh", "value")):
renderer.queueNode(node, shader = self._non_printing_shader, uniforms = uniforms, transparent = True)
else:
renderer.queueNode(node, shader = self._non_printing_shader, transparent = True)
elif getattr(node, "_outside_buildarea", False):
renderer.queueNode(node, shader = self._disabled_shader)
elif per_mesh_stack and per_mesh_stack.getProperty("support_mesh", "value"):
# Render support meshes with a vertical stripe that is darker
shade_factor = 0.6
uniforms["diffuse_color_2"] = [
uniforms["diffuse_color"][0] * shade_factor,
uniforms["diffuse_color"][1] * shade_factor,
uniforms["diffuse_color"][2] * shade_factor,
1.0
]
renderer.queueNode(node, shader = self._support_mesh_shader, uniforms = uniforms)
else:
renderer.queueNode(node, shader = self._enabled_shader, uniforms = uniforms)
if node.callDecoration("isGroup") and Selection.isSelected(node):

View file

@ -0,0 +1,71 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Math.Vector import Vector
from UM.Tool import Tool
from PyQt5.QtCore import Qt, QUrl
from UM.Application import Application
from UM.Event import Event
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Settings.SettingInstance import SettingInstance
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
import os
import os.path
class SupportEraser(Tool):
def __init__(self):
super().__init__()
self._shortcut_key = Qt.Key_G
self._controller = Application.getInstance().getController()
def event(self, event):
super().event(event)
if event.type == Event.ToolActivateEvent:
# Load the remover mesh:
self._createEraserMesh()
# After we load the mesh, deactivate the tool again:
self.getController().setActiveTool(None)
def _createEraserMesh(self):
node = CuraSceneNode()
node.setName("Eraser")
node.setSelectable(True)
mesh = MeshBuilder()
mesh.addCube(10,10,10)
node.setMeshData(mesh.build())
# Place the cube in the platform. Do it manually so it works if the "automatic drop models" is OFF
move_vector = Vector(0, 5, 0)
node.setPosition(move_vector)
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
node.addDecorator(SettingOverrideDecorator())
node.addDecorator(BuildPlateDecorator(active_build_plate))
node.addDecorator(SliceableObjectDecorator())
stack = node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
if not stack:
node.addDecorator(SettingOverrideDecorator())
stack = node.callDecoration("getStack")
settings = stack.getTop()
if not (settings.getInstance("anti_overhang_mesh") and settings.getProperty("anti_overhang_mesh", "value")):
definition = stack.getSettingDefinition("anti_overhang_mesh")
new_instance = SettingInstance(definition, settings)
new_instance.setProperty("value", True)
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
scene = self._controller.getScene()
op = AddSceneNodeOperation(node, scene.getRoot())
op.push()
Application.getInstance().getController().getScene().sceneChanged.emit(node)

View file

@ -0,0 +1,20 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import SupportEraser
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("uranium")
def getMetaData():
return {
"tool": {
"name": i18n_catalog.i18nc("@label", "Support Blocker"),
"description": i18n_catalog.i18nc("@info:tooltip", "Create a volume in which supports are not printed."),
"icon": "tool_icon.svg",
"weight": 4
}
}
def register(app):
return { "tool": SupportEraser.SupportEraser() }

View file

@ -0,0 +1,8 @@
{
"name": "Support Eraser",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Creates an eraser mesh to block the printing of support in certain places",
"api": 4,
"i18n-catalog": "cura"
}

View file

@ -0,0 +1,11 @@
<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch -->
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Cura-Icon" fill="#000000">
<path d="M19,27 L12,27 L3,27 L3,3 L12,3 L12,11 L19,11 L19,19 L27,19 L27,27 L19,27 Z M4,4 L4,26 L11,26 L11,4 L4,4 Z" id="Combined-Shape"></path>
<polygon id="Path" points="10 17.1441441 9.18918919 17.954955 7.52252252 16.3333333 5.85585586 18 5.04504505 17.1891892 6.66666667 15.4774775 5 13.8558559 5.81081081 13.045045 7.52252252 14.6666667 9.18918919 13 10 13.8108108 8.33333333 15.4774775"></polygon>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 866 B

View file

@ -0,0 +1,56 @@
#Copyright (c) 2018 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
from Charon.VirtualFile import VirtualFile #To open UFP files.
from Charon.OpenMode import OpenMode #To indicate that we want to write to UFP files.
from io import StringIO #For converting g-code to bytes.
from UM.Application import Application
from UM.Logger import Logger
from UM.Mesh.MeshWriter import MeshWriter #The writer we need to implement.
from UM.PluginRegistry import PluginRegistry #To get the g-code writer.
from PyQt5.QtCore import QBuffer
from cura.Snapshot import Snapshot
class UFPWriter(MeshWriter):
def __init__(self):
super().__init__()
self._snapshot = None
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._createSnapshot)
def _createSnapshot(self, *args):
# must be called from the main thread because of OpenGL
Logger.log("d", "Creating thumbnail image...")
self._snapshot = Snapshot.snapshot(width = 300, height = 300)
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
archive = VirtualFile()
archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly)
#Store the g-code from the scene.
archive.addContentType(extension = "gcode", mime_type = "text/x-gcode")
gcode_textio = StringIO() #We have to convert the g-code into bytes.
PluginRegistry.getInstance().getPluginObject("GCodeWriter").write(gcode_textio, None)
gcode = archive.getStream("/3D/model.gcode")
gcode.write(gcode_textio.getvalue().encode("UTF-8"))
archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode")
#Store the thumbnail.
if self._snapshot:
archive.addContentType(extension = "png", mime_type = "image/png")
thumbnail = archive.getStream("/Metadata/thumbnail.png")
thumbnail_buffer = QBuffer()
thumbnail_buffer.open(QBuffer.ReadWrite)
thumbnail_image = self._snapshot
thumbnail_image.save(thumbnail_buffer, "PNG")
thumbnail.write(thumbnail_buffer.data())
archive.addRelation(virtual_path = "/Metadata/thumbnail.png", relation_type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail", origin = "/3D/model.gcode")
else:
Logger.log("d", "Thumbnail not created, cannot save it")
archive.close()
return True

View file

@ -0,0 +1,38 @@
#Copyright (c) 2018 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
import sys
from UM.Logger import Logger
try:
from . import UFPWriter
except ImportError:
Logger.log("w", "Could not import UFPWriter; libCharon may be missing")
from UM.i18n import i18nCatalog #To translate the file format description.
from UM.Mesh.MeshWriter import MeshWriter #For the binary mode flag.
i18n_catalog = i18nCatalog("cura")
def getMetaData():
if "UFPWriter.UFPWriter" not in sys.modules:
return {}
return {
"mesh_writer": {
"output": [
{
"mime_type": "application/x-ufp",
"mode": MeshWriter.OutputMode.BinaryMode,
"extension": "ufp",
"description": i18n_catalog.i18nc("@item:inlistbox", "Ultimaker Format Package")
}
]
}
}
def register(app):
if "UFPWriter.UFPWriter" not in sys.modules:
return {}
return { "mesh_writer": UFPWriter.UFPWriter() }

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

View file

@ -0,0 +1,8 @@
{
"name": "UFP Writer",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for writing Ultimaker Format Packages.",
"api": 4,
"i18n-catalog": "cura"
}

View file

@ -10,13 +10,12 @@ Component
{
id: base
property var manager: Cura.MachineManager.printerOutputDevices[0]
anchors.fill: parent
color: UM.Theme.getColor("viewport_background")
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
visible: manager != null
anchors.fill: parent
color: UM.Theme.getColor("viewport_background")
UM.I18nCatalog
{
@ -97,7 +96,7 @@ Component
}
Label
{
text: manager.numJobsPrinting
text: manager.activePrintJobs.length
font: UM.Theme.getFont("small")
anchors.right: parent.right
}
@ -114,7 +113,7 @@ Component
}
Label
{
text: manager.numJobsQueued
text: manager.queuedPrintJobs.length
font: UM.Theme.getFont("small")
anchors.right: parent.right
}

View file

@ -12,10 +12,10 @@ Component
width: maximumWidth
height: maximumHeight
color: UM.Theme.getColor("viewport_background")
property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight")
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
UM.I18nCatalog
{
id: catalog
@ -33,9 +33,9 @@ Component
horizontalCenter: parent.horizontalCenter
}
text: OutputDevice.connectedPrinters.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
text: OutputDevice.printers.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
visible: OutputDevice.connectedPrinters.length == 0
visible: OutputDevice.printers.length == 0
}
Item
@ -46,23 +46,28 @@ Component
width: Math.min(800 * screenScaleFactor, maximumWidth)
height: children.height
visible: OutputDevice.connectedPrinters.length != 0
visible: OutputDevice.printers.length != 0
Label
{
id: addRemovePrintersLabel
anchors.right: parent.right
text: "Add / remove printers"
text: catalog.i18nc("@label link to connect manager", "Add/Remove printers")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
}
MouseArea
{
anchors.fill: addRemovePrintersLabel
hoverEnabled: true
onClicked: Cura.MachineManager.printerOutputDevices[0].openPrinterControlPanel()
onEntered: addRemovePrintersLabel.font.underline = true
onExited: addRemovePrintersLabel.font.underline = false
}
}
ScrollView
{
id: printerScrollView
@ -79,7 +84,7 @@ Component
anchors.fill: parent
spacing: -UM.Theme.getSize("default_lining").height
model: OutputDevice.connectedPrinters
model: OutputDevice.printers
delegate: PrinterInfoBlock
{
@ -95,7 +100,7 @@ Component
PrinterVideoStream
{
visible: OutputDevice.selectedPrinterName != ""
visible: OutputDevice.activePrinter != null
anchors.fill:parent
}
}

View file

@ -0,0 +1,453 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog
from UM.Message import Message
from UM.Qt.Duration import Duration, DurationFormat
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutput.NetworkCamera import NetworkCamera
from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
from time import time
from datetime import datetime
from typing import Optional
import json
import os
i18n_catalog = i18nCatalog("cura")
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
printJobsChanged = pyqtSignal()
activePrinterChanged = pyqtSignal()
# This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
# Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
clusterPrintersChanged = pyqtSignal()
def __init__(self, device_id, address, properties, parent = None):
super().__init__(device_id = device_id, address = address, properties=properties, parent = parent)
self._api_prefix = "/cluster-api/v1/"
self._number_of_extruders = 2
self._print_jobs = []
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml")
self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml")
# See comments about this hack with the clusterPrintersChanged signal
self.printersChanged.connect(self.clusterPrintersChanged)
self._accepts_commands = True
# Cluster does not have authentication, so default to authenticated
self._authentication_state = AuthState.Authenticated
self._error_message = None
self._progress_message = None
self._active_printer = None # type: Optional[PrinterOutputModel]
self._printer_selection_dialog = None
self.setPriority(3) # Make sure the output device gets selected above local file output
self.setName(self._id)
self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network"))
self._printer_uuid_to_unique_name_mapping = {}
self._finished_jobs = []
self._cluster_size = int(properties.get(b"cluster_size", 0))
self._latest_reply_handler = None
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
self.writeStarted.emit(self)
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", [])
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
# Unable to find g-code. Nothing to send
return
self._gcode = gcode_list
if len(self._printers) > 1:
self._spawnPrinterSelectionDialog()
else:
self.sendPrintJob()
# Notify the UI that a switch to the print monitor should happen
Application.getInstance().getController().setActiveStage("MonitorStage")
def _spawnPrinterSelectionDialog(self):
if self._printer_selection_dialog is None:
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml")
self._printer_selection_dialog = Application.getInstance().createQmlComponent(path, {"OutputDevice": self})
if self._printer_selection_dialog is not None:
self._printer_selection_dialog.show()
@pyqtProperty(int, constant=True)
def clusterSize(self):
return self._cluster_size
@pyqtSlot()
@pyqtSlot(str)
def sendPrintJob(self, target_printer = ""):
Logger.log("i", "Sending print job to printer.")
if self._sending_gcode:
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
self._error_message.show()
return
self._sending_gcode = True
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
i18n_catalog.i18nc("@info:title", "Sending Data"))
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
self._progress_message.show()
compressed_gcode = self._compressGCode()
if compressed_gcode is None:
# Abort was called.
return
parts = []
# If a specific printer was selected, it should be printed with that machine.
if target_printer:
target_printer = self._printer_uuid_to_unique_name_mapping[target_printer]
parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain"))
# Add user name to the print_job
parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))
file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, compressed_gcode))
self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)
@pyqtProperty(QObject, notify=activePrinterChanged)
def activePrinter(self) -> Optional["PrinterOutputModel"]:
return self._active_printer
@pyqtSlot(QObject)
def setActivePrinter(self, printer):
if self._active_printer != printer:
if self._active_printer and self._active_printer.camera:
self._active_printer.camera.stop()
self._active_printer = printer
self.activePrinterChanged.emit()
def _onPostPrintJobFinished(self, reply):
self._progress_message.hide()
self._compressing_gcode = False
self._sending_gcode = False
def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
if bytes_total > 0:
new_progress = bytes_sent / bytes_total * 100
# Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
# timeout responses if this happens.
self._last_response_time = time()
if new_progress > self._progress_message.getProgress():
self._progress_message.show() # Ensure that the message is visible.
self._progress_message.setProgress(bytes_sent / bytes_total * 100)
else:
self._progress_message.setProgress(0)
self._progress_message.hide()
def _progressMessageActionTriggered(self, message_id=None, action_id=None):
if action_id == "Abort":
Logger.log("d", "User aborted sending print to remote.")
self._progress_message.hide()
self._compressing_gcode = False
self._sending_gcode = False
Application.getInstance().getController().setActiveStage("PrepareStage")
# After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request
# the "reply" should be disconnected
if self._latest_reply_handler:
self._latest_reply_handler.disconnect()
@pyqtSlot()
def openPrintJobControlPanel(self):
Logger.log("d", "Opening print job control panel...")
QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
@pyqtSlot()
def openPrinterControlPanel(self):
Logger.log("d", "Opening printer control panel...")
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
@pyqtProperty("QVariantList", notify=printJobsChanged)
def printJobs(self):
return self._print_jobs
@pyqtProperty("QVariantList", notify=printJobsChanged)
def queuedPrintJobs(self):
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is None or print_job.state == "queued"]
@pyqtProperty("QVariantList", notify=printJobsChanged)
def activePrintJobs(self):
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"]
@pyqtProperty("QVariantList", notify=clusterPrintersChanged)
def connectedPrintersTypeCount(self):
printer_count = {}
for printer in self._printers:
if printer.type in printer_count:
printer_count[printer.type] += 1
else:
printer_count[printer.type] = 1
result = []
for machine_type in printer_count:
result.append({"machine_type": machine_type, "count": printer_count[machine_type]})
return result
@pyqtSlot(int, result=str)
def formatDuration(self, seconds):
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
@pyqtSlot(int, result=str)
def getTimeCompleted(self, time_remaining):
current_time = time()
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)
@pyqtSlot(int, result=str)
def getDateCompleted(self, time_remaining):
current_time = time()
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
def _printJobStateChanged(self):
username = self._getUserName()
if username is None:
return # We only want to show notifications if username is set.
finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"]
newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username]
for job in newly_finished_jobs:
if job.assignedPrinter:
job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.".format(printer_name=job.assignedPrinter.name, job_name = job.name))
else:
job_completed_text = i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.".format(job_name = job.name))
job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
job_completed_message.show()
# Ensure UI gets updated
self.printJobsChanged.emit()
# Keep a list of all completed jobs so we know if something changed next time.
self._finished_jobs = finished_jobs
def _update(self):
if not super()._update():
return
self.get("printers/", onFinished=self._onGetPrintersDataFinished)
self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)
def _onGetPrintJobsFinished(self, reply: QNetworkReply):
if not checkValidGetReply(reply):
return
result = loadJsonFromReply(reply)
if result is None:
return
print_jobs_seen = []
job_list_changed = False
for print_job_data in result:
print_job = findByKey(self._print_jobs, print_job_data["uuid"])
if print_job is None:
print_job = self._createPrintJobModel(print_job_data)
job_list_changed = True
self._updatePrintJob(print_job, print_job_data)
if print_job.state != "queued": # Print job should be assigned to a printer.
if print_job.state == "failed":
# Print job was failed, so don't attach it to a printer.
printer = None
else:
printer = self._getPrinterByKey(print_job_data["printer_uuid"])
else: # The job can "reserve" a printer if some changes are required.
printer = self._getPrinterByKey(print_job_data["assigned_to"])
if printer:
printer.updateActivePrintJob(print_job)
print_jobs_seen.append(print_job)
# Check what jobs need to be removed.
removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen]
for removed_job in removed_jobs:
job_list_changed |= self._removeJob(removed_job)
if job_list_changed:
self.printJobsChanged.emit() # Do a single emit for all print job changes.
def _onGetPrintersDataFinished(self, reply: QNetworkReply):
if not checkValidGetReply(reply):
return
result = loadJsonFromReply(reply)
if result is None:
return
printer_list_changed = False
printers_seen = []
for printer_data in result:
printer = findByKey(self._printers, printer_data["uuid"])
if printer is None:
printer = self._createPrinterModel(printer_data)
printer_list_changed = True
printers_seen.append(printer)
self._updatePrinter(printer, printer_data)
removed_printers = [printer for printer in self._printers if printer not in printers_seen]
for printer in removed_printers:
self._removePrinter(printer)
if removed_printers or printer_list_changed:
self.printersChanged.emit()
def _createPrinterModel(self, data):
printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
number_of_extruders=self._number_of_extruders)
printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream"))
self._printers.append(printer)
return printer
def _createPrintJobModel(self, data):
print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
key=data["uuid"], name= data["name"])
print_job.stateChanged.connect(self._printJobStateChanged)
self._print_jobs.append(print_job)
return print_job
def _updatePrintJob(self, print_job, data):
print_job.updateTimeTotal(data["time_total"])
print_job.updateTimeElapsed(data["time_elapsed"])
print_job.updateState(data["status"])
print_job.updateOwner(data["owner"])
def _updatePrinter(self, printer, data):
# For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
# Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]
printer.updateName(data["friendly_name"])
printer.updateKey(data["uuid"])
printer.updateType(data["machine_variant"])
if not data["enabled"]:
printer.updateState("disabled")
else:
printer.updateState(data["status"])
for index in range(0, self._number_of_extruders):
extruder = printer.extruders[index]
try:
extruder_data = data["configuration"][index]
except IndexError:
break
extruder.updateHotendID(extruder_data.get("print_core_id", ""))
material_data = extruder_data["material"]
if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
GUID=material_data["guid"])
if containers:
color = containers[0].getMetaDataEntry("color_code")
brand = containers[0].getMetaDataEntry("brand")
material_type = containers[0].getMetaDataEntry("material")
name = containers[0].getName()
else:
Logger.log("w",
"Unable to find material with guid {guid}. Using data as provided by cluster".format(
guid=material_data["guid"]))
color = material_data["color"]
brand = material_data["brand"]
material_type = material_data["material"]
name = "Empty" if material_data["material"] == "empty" else "Unknown"
material = MaterialOutputModel(guid=material_data["guid"], type=material_type,
brand=brand, color=color, name=name)
extruder.updateActiveMaterial(material)
def _removeJob(self, job):
if job not in self._print_jobs:
return False
if job.assignedPrinter:
job.assignedPrinter.updateActivePrintJob(None)
job.stateChanged.disconnect(self._printJobStateChanged)
self._print_jobs.remove(job)
return True
def _removePrinter(self, printer):
self._printers.remove(printer)
if self._active_printer == printer:
self._active_printer = None
self.activePrinterChanged.emit()
def loadJsonFromReply(reply):
try:
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.logException("w", "Unable to decode JSON from reply.")
return
return result
def checkValidGetReply(reply):
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if status_code != 200:
Logger.log("w", "Got status code {status_code} while trying to get data".format(status_code=status_code))
return False
return True
def findByKey(list, key):
for item in list:
if item.key == key:
return item

View file

@ -0,0 +1,21 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
MYPY = False
if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
class ClusterUM3PrinterOutputController(PrinterOutputController):
def __init__(self, output_device):
super().__init__(output_device)
self.can_pre_heat_bed = False
self.can_control_manually = False
def setJobState(self, job: "PrintJobOutputModel", state: str):
data = "{\"action\": \"%s\"}" % state
self._output_device.put("print_jobs/%s/action" % job.key, data, onFinished=None)

View file

@ -12,7 +12,10 @@ from cura.MachineAction import MachineAction
catalog = i18nCatalog("cura")
class DiscoverUM3Action(MachineAction):
discoveredDevicesChanged = pyqtSignal()
def __init__(self):
super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network"))
self._qml_url = "DiscoverUM3Action.qml"
@ -25,24 +28,24 @@ class DiscoverUM3Action(MachineAction):
Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView)
self._last_zeroconf_event_time = time.time()
self._zeroconf_change_grace_period = 0.25 # Time to wait after a zeroconf service change before allowing a zeroconf reset
self._last_zero_conf_event_time = time.time()
printersChanged = pyqtSignal()
# Time to wait after a zero-conf service change before allowing a zeroconf reset
self._zero_conf_change_grace_period = 0.25
@pyqtSlot()
def startDiscovery(self):
if not self._network_plugin:
Logger.log("d", "Starting printer discovery.")
Logger.log("d", "Starting device discovery.")
self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
self._network_plugin.printerListChanged.connect(self._onPrinterDiscoveryChanged)
self.printersChanged.emit()
self._network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
self.discoveredDevicesChanged.emit()
## Re-filters the list of printers.
## Re-filters the list of devices.
@pyqtSlot()
def reset(self):
Logger.log("d", "Reset the list of found printers.")
self.printersChanged.emit()
Logger.log("d", "Reset the list of found devices.")
self.discoveredDevicesChanged.emit()
@pyqtSlot()
def restartDiscovery(self):
@ -51,43 +54,44 @@ class DiscoverUM3Action(MachineAction):
# It's most likely that the QML engine is still creating delegates, where the python side already deleted or
# garbage collected the data.
# Whatever the case, waiting a bit ensures that it doesn't crash.
if time.time() - self._last_zeroconf_event_time > self._zeroconf_change_grace_period:
if time.time() - self._last_zero_conf_event_time > self._zero_conf_change_grace_period:
if not self._network_plugin:
self.startDiscovery()
else:
self._network_plugin.startDiscovery()
@pyqtSlot(str, str)
def removeManualPrinter(self, key, address):
def removeManualDevice(self, key, address):
if not self._network_plugin:
return
self._network_plugin.removeManualPrinter(key, address)
self._network_plugin.removeManualDevice(key, address)
@pyqtSlot(str, str)
def setManualPrinter(self, key, address):
def setManualDevice(self, key, address):
if key != "":
# This manual printer replaces a current manual printer
self._network_plugin.removeManualPrinter(key)
self._network_plugin.removeManualDevice(key)
if address != "":
self._network_plugin.addManualPrinter(address)
self._network_plugin.addManualDevice(address)
def _onPrinterDiscoveryChanged(self, *args):
self._last_zeroconf_event_time = time.time()
self.printersChanged.emit()
def _onDeviceDiscoveryChanged(self, *args):
self._last_zero_conf_event_time = time.time()
self.discoveredDevicesChanged.emit()
@pyqtProperty("QVariantList", notify = printersChanged)
@pyqtProperty("QVariantList", notify = discoveredDevicesChanged)
def foundDevices(self):
if self._network_plugin:
# TODO: Check if this needs to stay.
if Application.getInstance().getGlobalContainerStack():
global_printer_type = Application.getInstance().getGlobalContainerStack().getBottom().getId()
else:
global_printer_type = "unknown"
printers = list(self._network_plugin.getPrinters().values())
printers = list(self._network_plugin.getDiscoveredDevices().values())
# TODO; There are still some testing printers that don't have a correct printer type, so don't filter out unkown ones just yet.
printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"]
#printers = [printer for printer in printers if printer.printerType == global_printer_type or printer.printerType == "unknown"]
printers.sort(key = lambda k: k.name)
return printers
else:

View file

@ -10,7 +10,7 @@ Cura.MachineAction
{
id: base
anchors.fill: parent;
property var selectedPrinter: null
property var selectedDevice: null
property bool completeProperties: true
Connections
@ -29,9 +29,9 @@ Cura.MachineAction
function connectToPrinter()
{
if(base.selectedPrinter && base.completeProperties)
if(base.selectedDevice && base.completeProperties)
{
var printerKey = base.selectedPrinter.getKey()
var printerKey = base.selectedDevice.key
if(manager.getStoredKey() != printerKey)
{
manager.setKey(printerKey);
@ -83,10 +83,10 @@ Cura.MachineAction
{
id: editButton
text: catalog.i18nc("@action:button", "Edit")
enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true"
enabled: base.selectedDevice != null && base.selectedDevice.getProperty("manual") == "true"
onClicked:
{
manualPrinterDialog.showDialog(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress);
manualPrinterDialog.showDialog(base.selectedDevice.key, base.selectedDevice.ipAddress);
}
}
@ -94,8 +94,8 @@ Cura.MachineAction
{
id: removeButton
text: catalog.i18nc("@action:button", "Remove")
enabled: base.selectedPrinter != null && base.selectedPrinter.getProperty("manual") == "true"
onClicked: manager.removeManualPrinter(base.selectedPrinter.getKey(), base.selectedPrinter.ipAddress)
enabled: base.selectedDevice != null && base.selectedDevice.getProperty("manual") == "true"
onClicked: manager.removeManualDevice(base.selectedDevice.key, base.selectedDevice.ipAddress)
}
Button
@ -114,7 +114,7 @@ Cura.MachineAction
Column
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
spacing: UM.Theme.getSize("default_margin").height
ScrollView
@ -139,7 +139,7 @@ Cura.MachineAction
{
var selectedKey = manager.getStoredKey();
for(var i = 0; i < model.length; i++) {
if(model[i].getKey() == selectedKey)
if(model[i].key == selectedKey)
{
currentIndex = i;
return
@ -151,9 +151,9 @@ Cura.MachineAction
currentIndex: -1
onCurrentIndexChanged:
{
base.selectedPrinter = listview.model[currentIndex];
base.selectedDevice = listview.model[currentIndex];
// Only allow connecting if the printer has responded to API query since the last refresh
base.completeProperties = base.selectedPrinter != null && base.selectedPrinter.getProperty("incomplete") != "true";
base.completeProperties = base.selectedDevice != null && base.selectedDevice.getProperty("incomplete") != "true";
}
Component.onCompleted: manager.startDiscovery()
delegate: Rectangle
@ -198,14 +198,14 @@ Cura.MachineAction
}
Column
{
width: Math.floor(parent.width * 0.5)
visible: base.selectedPrinter ? true : false
width: Math.round(parent.width * 0.5)
visible: base.selectedDevice ? true : false
spacing: UM.Theme.getSize("default_margin").height
Label
{
width: parent.width
wrapMode: Text.WordWrap
text: base.selectedPrinter ? base.selectedPrinter.name : ""
text: base.selectedDevice ? base.selectedDevice.name : ""
font: UM.Theme.getFont("large")
elide: Text.ElideRight
}
@ -216,27 +216,27 @@ Cura.MachineAction
columns: 2
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Type")
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text:
{
if(base.selectedPrinter)
if(base.selectedDevice)
{
if(base.selectedPrinter.printerType == "ultimaker3")
if(base.selectedDevice.printerType == "ultimaker3")
{
return catalog.i18nc("@label Printer name", "Ultimaker 3")
} else if(base.selectedPrinter.printerType == "ultimaker3_extended")
return catalog.i18nc("@label", "Ultimaker 3")
} else if(base.selectedDevice.printerType == "ultimaker3_extended")
{
return catalog.i18nc("@label Printer name", "Ultimaker 3 Extended")
return catalog.i18nc("@label", "Ultimaker 3 Extended")
} else
{
return catalog.i18nc("@label Printer name", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
return catalog.i18nc("@label", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
}
}
else
@ -247,27 +247,27 @@ Cura.MachineAction
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Firmware version")
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: base.selectedPrinter ? base.selectedPrinter.firmwareVersion : ""
text: base.selectedDevice ? base.selectedDevice.firmwareVersion : ""
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Address")
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: base.selectedPrinter ? base.selectedPrinter.ipAddress : ""
text: base.selectedDevice ? base.selectedDevice.ipAddress : ""
}
}
@ -277,17 +277,17 @@ Cura.MachineAction
wrapMode: Text.WordWrap
text:{
// The property cluster size does not exist for older UM3 devices.
if(!base.selectedPrinter || base.selectedPrinter.clusterSize == null || base.selectedPrinter.clusterSize == 1)
if(!base.selectedDevice || base.selectedDevice.clusterSize == null || base.selectedDevice.clusterSize == 1)
{
return "";
}
else if (base.selectedPrinter.clusterSize === 0)
else if (base.selectedDevice.clusterSize === 0)
{
return catalog.i18nc("@label", "This printer is not set up to host a group of Ultimaker 3 printers.");
}
else
{
return catalog.i18nc("@label", "This printer is the host for a group of %1 Ultimaker 3 printers.".arg(base.selectedPrinter.clusterSize));
return catalog.i18nc("@label", "This printer is the host for a group of %1 Ultimaker 3 printers.".arg(base.selectedDevice.clusterSize));
}
}
@ -296,14 +296,14 @@ Cura.MachineAction
{
width: parent.width
wrapMode: Text.WordWrap
visible: base.selectedPrinter != null && !base.completeProperties
visible: base.selectedDevice != null && !base.completeProperties
text: catalog.i18nc("@label", "The printer at this address has not yet responded." )
}
Button
{
text: catalog.i18nc("@action:button", "Connect")
enabled: (base.selectedPrinter && base.completeProperties) ? true : false
enabled: (base.selectedDevice && base.completeProperties) ? true : false
onClicked: connectToPrinter()
}
}
@ -337,7 +337,7 @@ Cura.MachineAction
onAccepted:
{
manager.setManualPrinter(printerKey, addressText)
manager.setManualDevice(printerKey, addressText)
}
Column {

View file

@ -0,0 +1,636 @@
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutput.NetworkCamera import NetworkCamera
from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.ExtruderManager import ExtruderManager
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Application import Application
from UM.i18n import i18nCatalog
from UM.Message import Message
from PyQt5.QtNetwork import QNetworkRequest
from PyQt5.QtCore import QTimer, QCoreApplication
from PyQt5.QtWidgets import QMessageBox
from .LegacyUM3PrinterOutputController import LegacyUM3PrinterOutputController
from time import time
import json
import os
i18n_catalog = i18nCatalog("cura")
## This is the output device for the "Legacy" API of the UM3. All firmware before 4.0.1 uses this API.
# Everything after that firmware uses the ClusterUM3Output.
# The Legacy output device can only have one printer (whereas the cluster can have 0 to n).
#
# Authentication is done in a number of steps;
# 1. Request an id / key pair by sending the application & user name. (state = authRequested)
# 2. Machine sends this back and will display an approve / deny message on screen. (state = AuthReceived)
# 3. OutputDevice will poll if the button was pressed.
# 4. At this point the machine either has the state Authenticated or AuthenticationDenied.
# 5. As a final step, we verify the authentication, as this forces the QT manager to setup the authenticator.
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
def __init__(self, device_id, address: str, properties, parent = None):
super().__init__(device_id = device_id, address = address, properties = properties, parent = parent)
self._api_prefix = "/api/v1/"
self._number_of_extruders = 2
self._authentication_id = None
self._authentication_key = None
self._authentication_counter = 0
self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min)
self._authentication_timer = QTimer()
self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval
self._authentication_timer.setSingleShot(False)
self._authentication_timer.timeout.connect(self._onAuthenticationTimer)
# The messages are created when connect is called the first time.
# This ensures that the messages are only created for devices that actually want to connect.
self._authentication_requested_message = None
self._authentication_failed_message = None
self._authentication_succeeded_message = None
self._not_authenticated_message = None
self.authenticationStateChanged.connect(self._onAuthenticationStateChanged)
self.setPriority(3) # Make sure the output device gets selected above local file output
self.setName(self._id)
self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
self.setIconName("print")
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml")
self._output_controller = LegacyUM3PrinterOutputController(self)
def _onAuthenticationStateChanged(self):
# We only accept commands if we are authenticated.
self._setAcceptsCommands(self._authentication_state == AuthState.Authenticated)
if self._authentication_state == AuthState.Authenticated:
self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network."))
elif self._authentication_state == AuthState.AuthenticationRequested:
self.setConnectionText(i18n_catalog.i18nc("@info:status",
"Connected over the network. Please approve the access request on the printer."))
elif self._authentication_state == AuthState.AuthenticationDenied:
self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. No access to control the printer."))
def _setupMessages(self):
self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status",
"Access to the printer requested. Please approve the request on the printer"),
lifetime=0, dismissable=False, progress=0,
title=i18n_catalog.i18nc("@info:title",
"Authentication status"))
self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", ""),
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None,
i18n_catalog.i18nc("@info:tooltip", "Re-send the access request"))
self._authentication_failed_message.actionTriggered.connect(self._messageCallback)
self._authentication_succeeded_message = Message(
i18n_catalog.i18nc("@info:status", "Access to the printer accepted"),
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
self._not_authenticated_message = Message(
i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."),
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"),
None, i18n_catalog.i18nc("@info:tooltip",
"Send access request to the printer"))
self._not_authenticated_message.actionTriggered.connect(self._messageCallback)
def _messageCallback(self, message_id=None, action_id="Retry"):
if action_id == "Request" or action_id == "Retry":
if self._authentication_failed_message:
self._authentication_failed_message.hide()
if self._not_authenticated_message:
self._not_authenticated_message.hide()
self._requestAuthentication()
def connect(self):
super().connect()
self._setupMessages()
global_container = Application.getInstance().getGlobalContainerStack()
if global_container:
self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None)
self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None)
def close(self):
super().close()
if self._authentication_requested_message:
self._authentication_requested_message.hide()
if self._authentication_failed_message:
self._authentication_failed_message.hide()
if self._authentication_succeeded_message:
self._authentication_succeeded_message.hide()
self._sending_gcode = False
self._compressing_gcode = False
self._authentication_timer.stop()
## Send all material profiles to the printer.
def _sendMaterialProfiles(self):
Logger.log("i", "Sending material profiles to printer")
# TODO: Might want to move this to a job...
for container in ContainerRegistry.getInstance().findInstanceContainers(type="material"):
try:
xml_data = container.serialize()
if xml_data == "" or xml_data is None:
continue
names = ContainerManager.getInstance().getLinkedMaterials(container.getId())
if names:
# There are other materials that share this GUID.
if not container.isReadOnly():
continue # If it's not readonly, it's created by user, so skip it.
file_name = "none.xml"
self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), onFinished=None)
except NotImplementedError:
# If the material container is not the most "generic" one it can't be serialized an will raise a
# NotImplementedError. We can simply ignore these.
pass
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
if not self.activePrinter:
# No active printer. Unable to write
return
if self.activePrinter.state not in ["idle", ""]:
# Printer is not able to accept commands.
return
if self._authentication_state != AuthState.Authenticated:
# Not authenticated, so unable to send job.
return
self.writeStarted.emit(self)
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", [])
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
# Unable to find g-code. Nothing to send
return
self._gcode = gcode_list
errors = self._checkForErrors()
if errors:
text = i18n_catalog.i18nc("@label", "Unable to start a new print job.")
informative_text = i18n_catalog.i18nc("@label",
"There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. "
"Please resolve this issues before continuing.")
detailed_text = ""
for error in errors:
detailed_text += error + "\n"
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
text,
informative_text,
detailed_text,
buttons=QMessageBox.Ok,
icon=QMessageBox.Critical,
callback = self._messageBoxCallback
)
return # Don't continue; Errors must block sending the job to the printer.
# There might be multiple things wrong with the configuration. Check these before starting.
warnings = self._checkForWarnings()
if warnings:
text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?")
informative_text = i18n_catalog.i18nc("@label",
"There is a mismatch between the configuration or calibration of the printer and Cura. "
"For the best result, always slice for the PrintCores and materials that are inserted in your printer.")
detailed_text = ""
for warning in warnings:
detailed_text += warning + "\n"
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
text,
informative_text,
detailed_text,
buttons=QMessageBox.Yes + QMessageBox.No,
icon=QMessageBox.Question,
callback=self._messageBoxCallback
)
return
# No warnings or errors, so we're good to go.
self._startPrint()
# Notify the UI that a switch to the print monitor should happen
Application.getInstance().getController().setActiveStage("MonitorStage")
def _startPrint(self):
Logger.log("i", "Sending print job to printer.")
if self._sending_gcode:
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
self._error_message.show()
return
self._sending_gcode = True
self._send_gcode_start = time()
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
i18n_catalog.i18nc("@info:title", "Sending Data"))
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
self._progress_message.show()
compressed_gcode = self._compressGCode()
if compressed_gcode is None:
# Abort was called.
return
file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode,
onFinished=self._onPostPrintJobFinished)
return
def _progressMessageActionTriggered(self, message_id=None, action_id=None):
if action_id == "Abort":
Logger.log("d", "User aborted sending print to remote.")
self._progress_message.hide()
self._compressing_gcode = False
self._sending_gcode = False
Application.getInstance().getController().setActiveStage("PrepareStage")
def _onPostPrintJobFinished(self, reply):
self._progress_message.hide()
self._sending_gcode = False
def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
if bytes_total > 0:
new_progress = bytes_sent / bytes_total * 100
# Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
# timeout responses if this happens.
self._last_response_time = time()
if new_progress > self._progress_message.getProgress():
self._progress_message.show() # Ensure that the message is visible.
self._progress_message.setProgress(bytes_sent / bytes_total * 100)
else:
self._progress_message.setProgress(0)
self._progress_message.hide()
def _messageBoxCallback(self, button):
def delayedCallback():
if button == QMessageBox.Yes:
self._startPrint()
else:
Application.getInstance().getController().setActiveStage("PrepareStage")
# For some unknown reason Cura on OSX will hang if we do the call back code
# immediately without first returning and leaving QML's event system.
QTimer.singleShot(100, delayedCallback)
def _checkForErrors(self):
errors = []
print_information = Application.getInstance().getPrintInformation()
if not print_information.materialLengths:
Logger.log("w", "There is no material length information. Unable to check for errors.")
return errors
for index, extruder in enumerate(self.activePrinter.extruders):
# Due to airflow issues, both slots must be loaded, regardless if they are actually used or not.
if extruder.hotendID == "":
# No Printcore loaded.
errors.append(i18n_catalog.i18nc("@info:status", "No Printcore loaded in slot {slot_number}".format(slot_number=index + 1)))
if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
# The extruder is by this print.
if extruder.activeMaterial is None:
# No active material
errors.append(i18n_catalog.i18nc("@info:status", "No material loaded in slot {slot_number}".format(slot_number=index + 1)))
return errors
def _checkForWarnings(self):
warnings = []
print_information = Application.getInstance().getPrintInformation()
if not print_information.materialLengths:
Logger.log("w", "There is no material length information. Unable to check for warnings.")
return warnings
extruder_manager = ExtruderManager.getInstance()
for index, extruder in enumerate(self.activePrinter.extruders):
if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
# The extruder is by this print.
# TODO: material length check
# Check if the right Printcore is active.
variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"})
if variant:
if variant.getName() != extruder.hotendID:
warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}".format(cura_printcore_name = variant.getName(), remote_printcore_name = extruder.hotendID, extruder_id = index + 1)))
else:
Logger.log("w", "Unable to find variant.")
# Check if the right material is loaded.
local_material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"})
if local_material:
if extruder.activeMaterial.guid != local_material.getMetaDataEntry("GUID"):
Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID"))
warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(local_material.getName(), extruder.activeMaterial.name, index + 1))
else:
Logger.log("w", "Unable to find material.")
return warnings
def _update(self):
if not super()._update():
return
if self._authentication_state == AuthState.NotAuthenticated:
if self._authentication_id is None and self._authentication_key is None:
# This machine doesn't have any authentication, so request it.
self._requestAuthentication()
elif self._authentication_id is not None and self._authentication_key is not None:
# We have authentication info, but we haven't checked it out yet. Do so now.
self._verifyAuthentication()
elif self._authentication_state == AuthState.AuthenticationReceived:
# We have an authentication, but it's not confirmed yet.
self._checkAuthentication()
# We don't need authentication for requesting info, so we can go right ahead with requesting this.
self.get("printer", onFinished=self._onGetPrinterDataFinished)
self.get("print_job", onFinished=self._onGetPrintJobFinished)
def _resetAuthenticationRequestedMessage(self):
if self._authentication_requested_message:
self._authentication_requested_message.hide()
self._authentication_timer.stop()
self._authentication_counter = 0
def _onAuthenticationTimer(self):
self._authentication_counter += 1
self._authentication_requested_message.setProgress(
self._authentication_counter / self._max_authentication_counter * 100)
if self._authentication_counter > self._max_authentication_counter:
self._authentication_timer.stop()
Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id)
self.setAuthenticationState(AuthState.AuthenticationDenied)
self._resetAuthenticationRequestedMessage()
self._authentication_failed_message.show()
def _verifyAuthentication(self):
Logger.log("d", "Attempting to verify authentication")
# This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator.
self.get("auth/verify", onFinished=self._onVerifyAuthenticationCompleted)
def _onVerifyAuthenticationCompleted(self, reply):
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if status_code == 401:
# Something went wrong; We somehow tried to verify authentication without having one.
Logger.log("d", "Attempted to verify auth without having one.")
self._authentication_id = None
self._authentication_key = None
self.setAuthenticationState(AuthState.NotAuthenticated)
elif status_code == 403 and self._authentication_state != AuthState.Authenticated:
# If we were already authenticated, we probably got an older message back all of the sudden. Drop that.
Logger.log("d",
"While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ",
self._authentication_state)
self.setAuthenticationState(AuthState.AuthenticationDenied)
self._authentication_failed_message.show()
elif status_code == 200:
self.setAuthenticationState(AuthState.Authenticated)
def _checkAuthentication(self):
Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
self.get("auth/check/" + str(self._authentication_id), onFinished=self._onCheckAuthenticationFinished)
def _onCheckAuthenticationFinished(self, reply):
if str(self._authentication_id) not in reply.url().toString():
Logger.log("w", "Got an old id response.")
# Got response for old authentication ID.
return
try:
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.")
return
if data.get("message", "") == "authorized":
Logger.log("i", "Authentication was approved")
self.setAuthenticationState(AuthState.Authenticated)
self._saveAuthentication()
# Double check that everything went well.
self._verifyAuthentication()
# Notify the user.
self._resetAuthenticationRequestedMessage()
self._authentication_succeeded_message.show()
elif data.get("message", "") == "unauthorized":
Logger.log("i", "Authentication was denied.")
self.setAuthenticationState(AuthState.AuthenticationDenied)
self._authentication_failed_message.show()
def _saveAuthentication(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
if "network_authentication_key" in global_container_stack.getMetaData():
global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key)
else:
global_container_stack.addMetaDataEntry("network_authentication_key", self._authentication_key)
if "network_authentication_id" in global_container_stack.getMetaData():
global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id)
else:
global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id)
# Force save so we are sure the data is not lost.
Application.getInstance().saveStack(global_container_stack)
Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id,
self._getSafeAuthKey())
else:
Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id,
self._getSafeAuthKey())
def _onRequestAuthenticationFinished(self, reply):
try:
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.")
self.setAuthenticationState(AuthState.NotAuthenticated)
return
self.setAuthenticationState(AuthState.AuthenticationReceived)
self._authentication_id = data["id"]
self._authentication_key = data["key"]
Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.",
self._authentication_id, self._getSafeAuthKey())
def _requestAuthentication(self):
self._authentication_requested_message.show()
self._authentication_timer.start()
# Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might
# give issues.
self._authentication_key = None
self._authentication_id = None
self.post("auth/request",
json.dumps({"application": "Cura-" + Application.getInstance().getVersion(),
"user": self._getUserName()}).encode(),
onFinished=self._onRequestAuthenticationFinished)
self.setAuthenticationState(AuthState.AuthenticationRequested)
def _onAuthenticationRequired(self, reply, authenticator):
if self._authentication_id is not None and self._authentication_key is not None:
Logger.log("d",
"Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s",
self._id, self._authentication_id, self._getSafeAuthKey())
authenticator.setUser(self._authentication_id)
authenticator.setPassword(self._authentication_key)
else:
Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._id)
def _onGetPrintJobFinished(self, reply):
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if not self._printers:
return # Ignore the data for now, we don't have info about a printer yet.
printer = self._printers[0]
if status_code == 200:
try:
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
return
if printer.activePrintJob is None:
print_job = PrintJobOutputModel(output_controller=self._output_controller)
printer.updateActivePrintJob(print_job)
else:
print_job = printer.activePrintJob
print_job.updateState(result["state"])
print_job.updateTimeElapsed(result["time_elapsed"])
print_job.updateTimeTotal(result["time_total"])
print_job.updateName(result["name"])
elif status_code == 404:
# No job found, so delete the active print job (if any!)
printer.updateActivePrintJob(None)
else:
Logger.log("w",
"Got status code {status_code} while trying to get printer data".format(status_code=status_code))
def materialHotendChangedMessage(self, callback):
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"),
i18n_catalog.i18nc("@label",
"Would you like to use your current printer configuration in Cura?"),
i18n_catalog.i18nc("@label",
"The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."),
buttons=QMessageBox.Yes + QMessageBox.No,
icon=QMessageBox.Question,
callback=callback
)
def _onGetPrinterDataFinished(self, reply):
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if status_code == 200:
try:
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid printer state message: Not valid JSON.")
return
if not self._printers:
# Quickest way to get the firmware version is to grab it from the zeroconf.
firmware_version = self._properties.get(b"firmware_version", b"").decode("utf-8")
self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version)]
self._printers[0].setCamera(NetworkCamera("http://" + self._address + ":8080/?action=stream"))
for extruder in self._printers[0].extruders:
extruder.activeMaterialChanged.connect(self.materialIdChanged)
extruder.hotendIDChanged.connect(self.hotendIdChanged)
self.printersChanged.emit()
# LegacyUM3 always has a single printer.
printer = self._printers[0]
printer.updateBedTemperature(result["bed"]["temperature"]["current"])
printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"])
printer.updateState(result["status"])
try:
# If we're still handling the request, we should ignore remote for a bit.
if not printer.getController().isPreheatRequestInProgress():
printer.updateIsPreheating(result["bed"]["pre_heat"]["active"])
except KeyError:
# Older firmwares don't support preheating, so we need to fake it.
pass
head_position = result["heads"][0]["position"]
printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"])
for index in range(0, self._number_of_extruders):
temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"]
extruder = printer.extruders[index]
extruder.updateTargetHotendTemperature(temperatures["target"])
extruder.updateHotendTemperature(temperatures["current"])
material_guid = result["heads"][0]["extruders"][index]["active_material"]["guid"]
if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid:
# Find matching material (as we need to set brand, type & color)
containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
GUID=material_guid)
if containers:
color = containers[0].getMetaDataEntry("color_code")
brand = containers[0].getMetaDataEntry("brand")
material_type = containers[0].getMetaDataEntry("material")
name = containers[0].getName()
else:
# Unknown material.
color = "#00000000"
brand = "Unknown"
material_type = "Unknown"
name = "Unknown"
material = MaterialOutputModel(guid=material_guid, type=material_type,
brand=brand, color=color, name = name)
extruder.updateActiveMaterial(material)
try:
hotend_id = result["heads"][0]["extruders"][index]["hotend"]["id"]
except KeyError:
hotend_id = ""
printer.extruders[index].updateHotendID(hotend_id)
else:
Logger.log("w",
"Got status code {status_code} while trying to get printer data".format(status_code = status_code))
## Convenience function to "blur" out all but the last 5 characters of the auth key.
# This can be used to debug print the key, without it compromising the security.
def _getSafeAuthKey(self):
if self._authentication_key is not None:
result = self._authentication_key[-5:]
result = "********" + result
return result
return self._authentication_key

View file

@ -0,0 +1,95 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from PyQt5.QtCore import QTimer
from UM.Version import Version
MYPY = False
if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
class LegacyUM3PrinterOutputController(PrinterOutputController):
def __init__(self, output_device):
super().__init__(output_device)
self._preheat_bed_timer = QTimer()
self._preheat_bed_timer.setSingleShot(True)
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
self._preheat_printer = None
self.can_control_manually = False
# Are we still waiting for a response about preheat?
# We need this so we can already update buttons, so it feels more snappy.
self._preheat_request_in_progress = False
def isPreheatRequestInProgress(self):
return self._preheat_request_in_progress
def setJobState(self, job: "PrintJobOutputModel", state: str):
data = "{\"target\": \"%s\"}" % state
self._output_device.put("print_job/state", data, onFinished=None)
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
data = str(temperature)
self._output_device.put("printer/bed/temperature/target", data, onFinished=self._onPutBedTemperatureCompleted)
def _onPutBedTemperatureCompleted(self, reply):
if Version(self._preheat_printer.firmwareVersion) < Version("3.5.92"):
# If it was handling a preheat, it isn't anymore.
self._preheat_request_in_progress = False
def _onPutPreheatBedCompleted(self, reply):
self._preheat_request_in_progress = False
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
head_pos = printer._head_position
new_x = head_pos.x + x
new_y = head_pos.y + y
new_z = head_pos.z + z
data = "{\n\"x\":%s,\n\"y\":%s,\n\"z\":%s\n}" %(new_x, new_y, new_z)
self._output_device.put("printer/heads/0/position", data, onFinished=None)
def homeBed(self, printer):
self._output_device.put("printer/heads/0/position/z", "0", onFinished=None)
def _onPreheatBedTimerFinished(self):
self.setTargetBedTemperature(self._preheat_printer, 0)
self._preheat_printer.updateIsPreheating(False)
self._preheat_request_in_progress = True
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
self.preheatBed(printer, temperature=0, duration=0)
self._preheat_bed_timer.stop()
printer.updateIsPreheating(False)
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
try:
temperature = round(temperature) # The API doesn't allow floating point.
duration = round(duration)
except ValueError:
return # Got invalid values, can't pre-heat.
if duration > 0:
data = """{"temperature": "%i", "timeout": "%i"}""" % (temperature, duration)
else:
data = """{"temperature": "%i"}""" % temperature
# Real bed pre-heating support is implemented from 3.5.92 and up.
if Version(printer.firmwareVersion) < Version("3.5.92"):
# No firmware-side duration support then, so just set target bed temp and set a timer.
self.setTargetBedTemperature(printer, temperature=temperature)
self._preheat_bed_timer.setInterval(duration * 1000)
self._preheat_bed_timer.start()
self._preheat_printer = printer
printer.updateIsPreheating(True)
return
self._output_device.put("printer/bed/pre_heat", data, onFinished = self._onPutPreheatBedCompleted)
printer.updateIsPreheating(True)
self._preheat_request_in_progress = True

View file

@ -6,40 +6,49 @@ import Cura 1.0 as Cura
Component
{
Image
Item
{
id: cameraImage
property bool proportionalHeight:
width: maximumWidth
height: maximumHeight
Image
{
if(sourceSize.height == 0 || maximumHeight == 0)
id: cameraImage
width: Math.min(sourceSize.width === 0 ? 800 * screenScaleFactor : sourceSize.width, maximumWidth)
height: Math.floor((sourceSize.height === 0 ? 600 * screenScaleFactor : sourceSize.height) * width / sourceSize.width)
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
z: 1
Component.onCompleted:
{
return true;
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
{
OutputDevice.activePrinter.camera.start()
}
}
return (sourceSize.width / sourceSize.height) > (maximumWidth / maximumHeight);
}
property real _width: Math.floor(Math.min(maximumWidth, sourceSize.width))
property real _height: Math.floor(Math.min(maximumHeight, sourceSize.height))
width: proportionalHeight ? _width : Math.floor(sourceSize.width * _height / sourceSize.height)
height: !proportionalHeight ? _height : Math.floor(sourceSize.height * _width / sourceSize.width)
anchors.horizontalCenter: parent.horizontalCenter
onVisibleChanged:
{
if(visible)
onVisibleChanged:
{
OutputDevice.startCamera()
} else
{
OutputDevice.stopCamera()
if(visible)
{
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
{
OutputDevice.activePrinter.camera.start()
}
} else
{
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
{
OutputDevice.activePrinter.camera.stop()
}
}
}
}
source:
{
if(OutputDevice.cameraImage)
source:
{
return OutputDevice.cameraImage;
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage)
{
return OutputDevice.activePrinter.camera.latestImage;
}
return "";
}
return "";
}
}
}

View file

@ -1,716 +0,0 @@
import datetime
import getpass
import gzip
import json
import os
import os.path
import time
from enum import Enum
from PyQt5.QtNetwork import QNetworkRequest, QHttpPart, QHttpMultiPart
from PyQt5.QtCore import QUrl, pyqtSlot, pyqtProperty, QCoreApplication, QTimer, pyqtSignal, QObject
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply
from UM.Application import Application
from UM.Logger import Logger
from UM.Message import Message
from UM.OutputDevice import OutputDeviceError
from UM.i18n import i18nCatalog
from UM.Qt.Duration import Duration, DurationFormat
from UM.PluginRegistry import PluginRegistry
from . import NetworkPrinterOutputDevice
i18n_catalog = i18nCatalog("cura")
class OutputStage(Enum):
ready = 0
uploading = 2
class NetworkClusterPrinterOutputDevice(NetworkPrinterOutputDevice.NetworkPrinterOutputDevice):
printJobsChanged = pyqtSignal()
printersChanged = pyqtSignal()
selectedPrinterChanged = pyqtSignal()
def __init__(self, key, address, properties, api_prefix):
super().__init__(key, address, properties, api_prefix)
# Store the address of the master.
self._master_address = address
name_property = properties.get(b"name", b"")
if name_property:
name = name_property.decode("utf-8")
else:
name = key
self._authentication_state = NetworkPrinterOutputDevice.AuthState.Authenticated # The printer is always authenticated
self.setName(name)
description = i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")
self.setShortDescription(description)
self.setDescription(description)
self._stage = OutputStage.ready
host_override = os.environ.get("CLUSTER_OVERRIDE_HOST", "")
if host_override:
Logger.log(
"w",
"Environment variable CLUSTER_OVERRIDE_HOST is set to [%s], cluster hosts are now set to this host",
host_override)
self._host = "http://" + host_override
else:
self._host = "http://" + address
# is the same as in NetworkPrinterOutputDevicePlugin
self._cluster_api_version = "1"
self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"
self._api_base_uri = self._host + self._cluster_api_prefix
self._file_name = None
self._progress_message = None
self._request = None
self._reply = None
# The main reason to keep the 'multipart' form data on the object
# is to prevent the Python GC from claiming it too early.
self._multipart = None
self._print_view = None
self._request_job = []
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml")
self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml")
self._print_jobs = []
self._print_job_by_printer_uuid = {}
self._print_job_by_uuid = {} # Print jobs by their own uuid
self._printers = []
self._printers_dict = {} # by unique_name
self._connected_printers_type_count = []
self._automatic_printer = {"unique_name": "", "friendly_name": "Automatic"} # empty unique_name IS automatic selection
self._selected_printer = self._automatic_printer
self._cluster_status_update_timer = QTimer()
self._cluster_status_update_timer.setInterval(5000)
self._cluster_status_update_timer.setSingleShot(False)
self._cluster_status_update_timer.timeout.connect(self._requestClusterStatus)
self._can_pause = True
self._can_abort = True
self._can_pre_heat_bed = False
self._can_control_manually = False
self._cluster_size = int(properties.get(b"cluster_size", 0))
self._cleanupRequest()
#These are texts that are to be translated for future features.
temporary_translation = i18n_catalog.i18n("This printer is not set up to host a group of connected Ultimaker 3 printers.")
temporary_translation2 = i18n_catalog.i18nc("Count is number of printers.", "This printer is the host for a group of {count} connected Ultimaker 3 printers.").format(count = 3)
temporary_translation3 = i18n_catalog.i18n("{printer_name} has finished printing '{job_name}'. Please collect the print and confirm clearing the build plate.") #When finished.
temporary_translation4 = i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.") #When configuration changed.
## No authentication, so requestAuthentication should do exactly nothing
@pyqtSlot()
def requestAuthentication(self, message_id = None, action_id = "Retry"):
pass # Cura Connect doesn't do any authorization
def setAuthenticationState(self, auth_state):
self._authentication_state = NetworkPrinterOutputDevice.AuthState.Authenticated # The printer is always authenticated
def _verifyAuthentication(self):
pass
def _checkAuthentication(self):
Logger.log("d", "_checkAuthentication Cura Connect - nothing to be done")
@pyqtProperty(QObject, notify=selectedPrinterChanged)
def controlItem(self):
# TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
if not self._control_item:
self._createControlViewFromQML()
name = self._selected_printer.get("friendly_name")
if name == self._automatic_printer.get("friendly_name") or name == "":
return self._control_item
# Let cura use the default.
return None
@pyqtSlot(int, result = str)
def getTimeCompleted(self, time_remaining):
current_time = time.time()
datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining)
return "{hour:02d}:{minute:02d}".format(hour = datetime_completed.hour, minute = datetime_completed.minute)
@pyqtSlot(int, result = str)
def getDateCompleted(self, time_remaining):
current_time = time.time()
datetime_completed = datetime.datetime.fromtimestamp(current_time + time_remaining)
return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
@pyqtProperty(int, constant = True)
def clusterSize(self):
return self._cluster_size
@pyqtProperty(str, notify=selectedPrinterChanged)
def name(self):
# Show the name of the selected printer.
# This is not the nicest way to do this, but changes to the Cura UI are required otherwise.
name = self._selected_printer.get("friendly_name")
if name != self._automatic_printer.get("friendly_name"):
return name
# Return name of cluster master.
return self._properties.get(b"name", b"").decode("utf-8")
def connect(self):
super().connect()
self._cluster_status_update_timer.start()
def close(self):
super().close()
self._cluster_status_update_timer.stop()
def _setJobState(self, job_state):
if not self._selected_printer:
return
selected_printer_uuid = self._printers_dict[self._selected_printer["unique_name"]]["uuid"]
if selected_printer_uuid not in self._print_job_by_printer_uuid:
return
print_job_uuid = self._print_job_by_printer_uuid[selected_printer_uuid]["uuid"]
url = QUrl(self._api_base_uri + "print_jobs/" + print_job_uuid + "/action")
put_request = QNetworkRequest(url)
put_request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
data = '{"action": "' + job_state + '"}'
self._manager.put(put_request, data.encode())
def _requestClusterStatus(self):
# TODO: Handle timeout. We probably want to know if the cluster is still reachable or not.
url = QUrl(self._api_base_uri + "printers/")
printers_request = QNetworkRequest(url)
self._addUserAgentHeader(printers_request)
self._manager.get(printers_request)
# See _finishedPrintersRequest()
if self._printers: # if printers is not empty
url = QUrl(self._api_base_uri + "print_jobs/")
print_jobs_request = QNetworkRequest(url)
self._addUserAgentHeader(print_jobs_request)
self._manager.get(print_jobs_request)
# See _finishedPrintJobsRequest()
def _finishedPrintJobsRequest(self, reply):
try:
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
return
self.setPrintJobs(json_data)
def _finishedPrintersRequest(self, reply):
try:
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.decoder.JSONDecodeError:
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
return
self.setPrinters(json_data)
def materialHotendChangedMessage(self, callback):
# When there is just one printer, the activate configuration option is enabled
if (self._cluster_size == 1):
super().materialHotendChangedMessage(callback = callback)
def _startCameraStream(self):
## Request new image
url = QUrl("http://" + self._printers_dict[self._selected_printer["unique_name"]]["ip_address"] + ":8080/?action=stream")
self._image_request = QNetworkRequest(url)
self._addUserAgentHeader(self._image_request)
self._image_reply = self._manager.get(self._image_request)
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
def spawnPrintView(self):
if self._print_view is None:
path = os.path.join(self._plugin_path, "PrintWindow.qml")
self._print_view = Application.getInstance().createQmlComponent(path, {"OutputDevice", self})
if self._print_view is not None:
self._print_view.show()
## Store job info, show Print view for settings
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
self._selected_printer = self._automatic_printer # reset to default option
self._request_job = [nodes, file_name, filter_by_machine, file_handler, kwargs]
if self._stage != OutputStage.ready:
if self._error_message:
self._error_message.hide()
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
self._error_message.show()
return
self.writeStarted.emit(self) # Allow postprocessing before sending data to the printer
if len(self._printers) > 1:
self.spawnPrintView() # Ask user how to print it.
elif len(self._printers) == 1:
# If there is only one printer, don't bother asking.
self.selectAutomaticPrinter()
self.sendPrintJob()
else:
# Cluster has no printers, warn the user of this.
if self._error_message:
self._error_message.hide()
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Unable to send new print job: this 3D printer is not (yet) set up to host a group of connected Ultimaker 3 printers."))
self._error_message.show()
## Actually send the print job, called from the dialog
# :param: require_printer_name: name of printer, or ""
@pyqtSlot()
def sendPrintJob(self):
nodes, file_name, filter_by_machine, file_handler, kwargs = self._request_job
require_printer_name = self._selected_printer["unique_name"]
self._send_gcode_start = time.time()
Logger.log("d", "Sending print job [%s] to host..." % file_name)
if self._stage != OutputStage.ready:
Logger.log("d", "Unable to send print job as the state is %s", self._stage)
raise OutputDeviceError.DeviceBusyError()
self._stage = OutputStage.uploading
self._file_name = "%s.gcode.gz" % file_name
self._showProgressMessage()
new_request = self._buildSendPrintJobHttpRequest(require_printer_name)
if new_request is None or self._stage != OutputStage.uploading:
return
self._request = new_request
self._reply = self._manager.post(self._request, self._multipart)
self._reply.uploadProgress.connect(self._onUploadProgress)
# See _finishedPostPrintJobRequest()
def _buildSendPrintJobHttpRequest(self, require_printer_name):
api_url = QUrl(self._api_base_uri + "print_jobs/")
request = QNetworkRequest(api_url)
# Create multipart request and add the g-code.
self._multipart = QHttpMultiPart(QHttpMultiPart.FormDataType)
# Add gcode
part = QHttpPart()
part.setHeader(QNetworkRequest.ContentDispositionHeader,
'form-data; name="file"; filename="%s"' % self._file_name)
gcode = getattr(Application.getInstance().getController().getScene(), "gcode_list")
compressed_gcode = self._compressGcode(gcode)
if compressed_gcode is None:
return None # User aborted print, so stop trying.
part.setBody(compressed_gcode)
self._multipart.append(part)
# require_printer_name "" means automatic
if require_printer_name:
self._multipart.append(self.__createKeyValueHttpPart("require_printer_name", require_printer_name))
user_name = self.__get_username()
if user_name is None:
user_name = "unknown"
self._multipart.append(self.__createKeyValueHttpPart("owner", user_name))
self._addUserAgentHeader(request)
return request
def _compressGcode(self, gcode):
self._compressing_print = True
batched_line = ""
max_chars_per_line = int(1024 * 1024 / 4) # 1 / 4 MB
byte_array_file_data = b""
def _compressDataAndNotifyQt(data_to_append):
compressed_data = gzip.compress(data_to_append.encode("utf-8"))
self._progress_message.setProgress(-1) # Tickle the message so that it's clear that it's still being used.
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
# Pretend that this is a response, as zipping might take a bit of time.
self._last_response_time = time.time()
return compressed_data
if gcode is None:
Logger.log("e", "Unable to find sliced gcode, returning empty.")
return byte_array_file_data
for line in gcode:
if not self._compressing_print:
self._progress_message.hide()
return None # Stop trying to zip, abort was called.
batched_line += line
# if the gcode was read from a gcode file, self._gcode will be a list of all lines in that file.
# Compressing line by line in this case is extremely slow, so we need to batch them.
if len(batched_line) < max_chars_per_line:
continue
byte_array_file_data += _compressDataAndNotifyQt(batched_line)
batched_line = ""
# Also compress the leftovers.
if batched_line:
byte_array_file_data += _compressDataAndNotifyQt(batched_line)
return byte_array_file_data
def __createKeyValueHttpPart(self, key, value):
metadata_part = QHttpPart()
metadata_part.setHeader(QNetworkRequest.ContentTypeHeader, 'text/plain')
metadata_part.setHeader(QNetworkRequest.ContentDispositionHeader, 'form-data; name="%s"' % (key))
metadata_part.setBody(bytearray(value, "utf8"))
return metadata_part
def __get_username(self):
try:
return getpass.getuser()
except:
Logger.log("d", "Could not get the system user name, returning 'unknown' instead.")
return None
def _finishedPrintJobPostRequest(self, reply):
self._stage = OutputStage.ready
if self._progress_message:
self._progress_message.hide()
self._progress_message = None
self.writeFinished.emit(self)
if reply.error():
self._showRequestFailedMessage(reply)
self.writeError.emit(self)
else:
self._showRequestSucceededMessage()
self.writeSuccess.emit(self)
self._cleanupRequest()
def _showRequestFailedMessage(self, reply):
if reply is not None:
Logger.log("w", "Unable to send print job to group {cluster_name}: {error_string} ({error})".format(
cluster_name = self.getName(),
error_string = str(reply.errorString()),
error = str(reply.error())))
error_message_template = i18n_catalog.i18nc("@info:status", "Unable to send print job to group {cluster_name}.")
message = Message(text=error_message_template.format(
cluster_name = self.getName()))
message.show()
def _showRequestSucceededMessage(self):
confirmation_message_template = i18n_catalog.i18nc(
"@info:status",
"Sent {file_name} to group {cluster_name}."
)
file_name = os.path.basename(self._file_name).split(".")[0]
message_text = confirmation_message_template.format(cluster_name = self.getName(), file_name = file_name)
message = Message(text=message_text)
button_text = i18n_catalog.i18nc("@action:button", "Show print jobs")
button_tooltip = i18n_catalog.i18nc("@info:tooltip", "Opens the print jobs interface in your browser.")
message.addAction("open_browser", button_text, "globe", button_tooltip)
message.actionTriggered.connect(self._onMessageActionTriggered)
message.show()
def setPrintJobs(self, print_jobs):
#TODO: hack, last seen messes up the check, so drop it.
for job in print_jobs:
del job["last_seen"]
# Strip any extensions
job["name"] = self._removeGcodeExtension(job["name"])
if self._print_jobs != print_jobs:
old_print_jobs = self._print_jobs
self._print_jobs = print_jobs
self._notifyFinishedPrintJobs(old_print_jobs, print_jobs)
self._notifyConfigurationChangeRequired(old_print_jobs, print_jobs)
# Yes, this is a hacky way of doing it, but it's quick and the API doesn't give the print job per printer
# for some reason. ugh.
self._print_job_by_printer_uuid = {}
self._print_job_by_uuid = {}
for print_job in print_jobs:
if "printer_uuid" in print_job and print_job["printer_uuid"] is not None:
self._print_job_by_printer_uuid[print_job["printer_uuid"]] = print_job
self._print_job_by_uuid[print_job["uuid"]] = print_job
self.printJobsChanged.emit()
def _removeGcodeExtension(self, name):
parts = name.split(".")
if parts[-1].upper() == "GZ":
parts = parts[:-1]
if parts[-1].upper() == "GCODE":
parts = parts[:-1]
return ".".join(parts)
def _notifyFinishedPrintJobs(self, old_print_jobs, new_print_jobs):
"""Notify the user when any of their print jobs have just completed.
Arguments:
old_print_jobs -- the previous list of print job status information as returned by the cluster REST API.
new_print_jobs -- the current list of print job status information as returned by the cluster REST API.
"""
if old_print_jobs is None:
return
username = self.__get_username()
if username is None:
return
our_old_print_jobs = self.__filterOurPrintJobs(old_print_jobs)
our_old_not_finished_print_jobs = [pj for pj in our_old_print_jobs if pj["status"] != "wait_cleanup"]
our_new_print_jobs = self.__filterOurPrintJobs(new_print_jobs)
our_new_finished_print_jobs = [pj for pj in our_new_print_jobs if pj["status"] == "wait_cleanup"]
old_not_finished_print_job_uuids = set([pj["uuid"] for pj in our_old_not_finished_print_jobs])
for print_job in our_new_finished_print_jobs:
if print_job["uuid"] in old_not_finished_print_job_uuids:
printer_name = self.__getPrinterNameFromUuid(print_job["printer_uuid"])
if printer_name is None:
printer_name = i18n_catalog.i18nc("@label Printer name", "Unknown")
message_text = (i18n_catalog.i18nc("@info:status",
"Printer '{printer_name}' has finished printing '{job_name}'.")
.format(printer_name=printer_name, job_name=print_job["name"]))
message = Message(text=message_text, title=i18n_catalog.i18nc("@info:status", "Print finished"))
Application.getInstance().showMessage(message)
Application.getInstance().showToastMessage(
i18n_catalog.i18nc("@info:status", "Print finished"),
message_text)
def __filterOurPrintJobs(self, print_jobs):
username = self.__get_username()
return [print_job for print_job in print_jobs if print_job["owner"] == username]
def _notifyConfigurationChangeRequired(self, old_print_jobs, new_print_jobs):
if old_print_jobs is None:
return
old_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(old_print_jobs))
new_change_required_print_jobs = self.__filterConfigChangePrintJobs(self.__filterOurPrintJobs(new_print_jobs))
old_change_required_print_job_uuids = set([pj["uuid"] for pj in old_change_required_print_jobs])
for print_job in new_change_required_print_jobs:
if print_job["uuid"] not in old_change_required_print_job_uuids:
printer_name = self.__getPrinterNameFromUuid(print_job["assigned_to"])
if printer_name is None:
# don't report on yet unknown printers
continue
message_text = (i18n_catalog.i18n("{printer_name} is reserved to print '{job_name}'. Please change the printer's configuration to match the job, for it to start printing.")
.format(printer_name=printer_name, job_name=print_job["name"]))
message = Message(text=message_text, title=i18n_catalog.i18nc("@label:status", "Action required"))
Application.getInstance().showMessage(message)
Application.getInstance().showToastMessage(
i18n_catalog.i18nc("@label:status", "Action required"),
message_text)
def __filterConfigChangePrintJobs(self, print_jobs):
return filter(self.__isConfigurationChangeRequiredPrintJob, print_jobs)
def __isConfigurationChangeRequiredPrintJob(self, print_job):
if print_job["status"] == "queued":
changes_required = print_job.get("configuration_changes_required", [])
return len(changes_required) != 0
return False
def __getPrinterNameFromUuid(self, printer_uuid):
for printer in self._printers:
if printer["uuid"] == printer_uuid:
return printer["friendly_name"]
return None
def setPrinters(self, printers):
if self._printers != printers:
self._connected_printers_type_count = []
printers_count = {}
self._printers = printers
self._printers_dict = dict((p["unique_name"], p) for p in printers) # for easy lookup by unique_name
for printer in printers:
variant = printer["machine_variant"]
if variant in printers_count:
printers_count[variant] += 1
else:
printers_count[variant] = 1
for type in printers_count:
self._connected_printers_type_count.append({"machine_type": type, "count": printers_count[type]})
self.printersChanged.emit()
@pyqtProperty("QVariantList", notify=printersChanged)
def connectedPrintersTypeCount(self):
return self._connected_printers_type_count
@pyqtProperty("QVariantList", notify=printersChanged)
def connectedPrinters(self):
return self._printers
@pyqtProperty(int, notify=printJobsChanged)
def numJobsPrinting(self):
num_jobs_printing = 0
for job in self._print_jobs:
if job["status"] in ["printing", "wait_cleanup", "sent_to_printer", "pre_print", "post_print"]:
num_jobs_printing += 1
return num_jobs_printing
@pyqtProperty(int, notify=printJobsChanged)
def numJobsQueued(self):
num_jobs_queued = 0
for job in self._print_jobs:
if job["status"] == "queued":
num_jobs_queued += 1
return num_jobs_queued
@pyqtProperty("QVariantMap", notify=printJobsChanged)
def printJobsByUUID(self):
return self._print_job_by_uuid
@pyqtProperty("QVariantMap", notify=printJobsChanged)
def printJobsByPrinterUUID(self):
return self._print_job_by_printer_uuid
@pyqtProperty("QVariantList", notify=printJobsChanged)
def printJobs(self):
return self._print_jobs
@pyqtProperty("QVariantList", notify=printersChanged)
def printers(self):
return [self._automatic_printer, ] + self._printers
@pyqtSlot(str, str)
def selectPrinter(self, unique_name, friendly_name):
self.stopCamera()
self._selected_printer = {"unique_name": unique_name, "friendly_name": friendly_name}
Logger.log("d", "Selected printer: %s %s", friendly_name, unique_name)
# TODO: Probably not the nicest way to do this. This needs to be done better at some point in time.
if unique_name == "":
self._address = self._master_address
else:
self._address = self._printers_dict[self._selected_printer["unique_name"]]["ip_address"]
self.selectedPrinterChanged.emit()
def _updateJobState(self, job_state):
name = self._selected_printer.get("friendly_name")
if name == "" or name == "Automatic":
# TODO: This is now a bit hacked; If no printer is selected, don't show job state.
if self._job_state != "":
self._job_state = ""
self.jobStateChanged.emit()
else:
if self._job_state != job_state:
self._job_state = job_state
self.jobStateChanged.emit()
@pyqtSlot()
def selectAutomaticPrinter(self):
self.stopCamera()
self._selected_printer = self._automatic_printer
self.selectedPrinterChanged.emit()
@pyqtProperty("QVariant", notify=selectedPrinterChanged)
def selectedPrinterName(self):
return self._selected_printer.get("unique_name", "")
def getPrintJobsUrl(self):
return self._host + "/print_jobs"
def getPrintersUrl(self):
return self._host + "/printers"
def _showProgressMessage(self):
progress_message_template = i18n_catalog.i18nc("@info:progress",
"Sending <filename>{file_name}</filename> to group {cluster_name}")
file_name = os.path.basename(self._file_name).split(".")[0]
self._progress_message = Message(progress_message_template.format(file_name = file_name, cluster_name = self.getName()), 0, False, -1)
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
self._progress_message.actionTriggered.connect(self._onMessageActionTriggered)
self._progress_message.show()
def _addUserAgentHeader(self, request):
request.setRawHeader(b"User-agent", b"CuraPrintClusterOutputDevice Plugin")
def _cleanupRequest(self):
self._request = None
self._stage = OutputStage.ready
self._file_name = None
def _onFinished(self, reply):
super()._onFinished(reply)
reply_url = reply.url().toString()
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if status_code == 500:
Logger.log("w", "Request to {url} returned a 500.".format(url = reply_url))
return
if reply.error() == QNetworkReply.ContentOperationNotPermittedError:
# It was probably "/api/v1/materials" for legacy UM3
return
if reply.error() == QNetworkReply.ContentNotFoundError:
# It was probably "/api/v1/print_job" for legacy UM3
return
if reply.operation() == QNetworkAccessManager.PostOperation:
if self._cluster_api_prefix + "print_jobs" in reply_url:
self._finishedPrintJobPostRequest(reply)
return
# We need to do this check *after* we process the post operation!
# If the sending of g-code is cancelled by the user it will result in an error, but we do need to handle this.
if reply.error() != QNetworkReply.NoError:
Logger.log("e", "After requesting [%s] we got a network error [%s]. Not processing anything...", reply_url, reply.error())
return
elif reply.operation() == QNetworkAccessManager.GetOperation:
if self._cluster_api_prefix + "print_jobs" in reply_url:
self._finishedPrintJobsRequest(reply)
elif self._cluster_api_prefix + "printers" in reply_url:
self._finishedPrintersRequest(reply)
@pyqtSlot()
def openPrintJobControlPanel(self):
Logger.log("d", "Opening print job control panel...")
QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl()))
@pyqtSlot()
def openPrinterControlPanel(self):
Logger.log("d", "Opening printer control panel...")
QDesktopServices.openUrl(QUrl(self.getPrintersUrl()))
def _onMessageActionTriggered(self, message, action):
if action == "open_browser":
QDesktopServices.openUrl(QUrl(self.getPrintJobsUrl()))
if action == "Abort":
Logger.log("d", "User aborted sending print to remote.")
self._progress_message.hide()
self._compressing_print = False
if self._reply:
self._reply.abort()
self._stage = OutputStage.ready
Application.getInstance().getController().setActiveStage("PrepareStage")
@pyqtSlot(int, result=str)
def formatDuration(self, seconds):
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
## For cluster below
def _get_plugin_directory_name(self):
current_file_absolute_path = os.path.realpath(__file__)
directory_path = os.path.dirname(current_file_absolute_path)
_, directory_name = os.path.split(directory_path)
return directory_name
@property
def _plugin_path(self):
return PluginRegistry.getInstance().getPluginPath(self._get_plugin_directory_name())

File diff suppressed because it is too large Load diff

View file

@ -1,361 +0,0 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import time
import json
from queue import Queue
from threading import Event, Thread
from PyQt5.QtCore import QObject, pyqtSlot
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
from UM.Application import Application
from UM.Logger import Logger
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from UM.Preferences import Preferences
from UM.Signal import Signal, signalemitter
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo # type: ignore
from . import NetworkPrinterOutputDevice, NetworkClusterPrinterOutputDevice
## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
# Zero-Conf is used to detect printers, which are saved in a dict.
# If we discover a printer that has the same key as the active machine instance a connection is made.
@signalemitter
class NetworkPrinterOutputDevicePlugin(QObject, OutputDevicePlugin):
def __init__(self):
super().__init__()
self._zero_conf = None
self._browser = None
self._printers = {}
self._cluster_printers_seen = {} # do not forget a cluster printer when we have seen one, to not 'downgrade' from Connect to legacy printer
self._api_version = "1"
self._api_prefix = "/api/v" + self._api_version + "/"
self._cluster_api_version = "1"
self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"
self._network_manager = QNetworkAccessManager()
self._network_manager.finished.connect(self._onNetworkRequestFinished)
# List of old printer names. This is used to ensure that a refresh of zeroconf does not needlessly forces
# authentication requests.
self._old_printers = []
self._excluded_addresses = ["127.0.0.1"] # Adding a list of not allowed IP addresses. At this moment, just localhost
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
self.addPrinterSignal.connect(self.addPrinter)
self.removePrinterSignal.connect(self.removePrinter)
Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)
# Get list of manual printers from preferences
self._preferences = Preferences.getInstance()
self._preferences.addPreference("um3networkprinting/manual_instances", "") # A comma-separated list of ip adresses or hostnames
self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")
self._network_requests_buffer = {} # store api responses until data is complete
# The zeroconf service changed requests are handled in a separate thread, so we can re-schedule the requests
# which fail to get detailed service info.
# Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
# them up and process them.
self._service_changed_request_queue = Queue()
self._service_changed_request_event = Event()
self._service_changed_request_thread = Thread(target = self._handleOnServiceChangedRequests,
daemon = True)
self._service_changed_request_thread.start()
addPrinterSignal = Signal()
removePrinterSignal = Signal()
printerListChanged = Signal()
## Start looking for devices on network.
def start(self):
self.startDiscovery()
def startDiscovery(self):
self.stop()
if self._browser:
self._browser.cancel()
self._browser = None
self._old_printers = [printer_name for printer_name in self._printers]
self._printers = {}
self.printerListChanged.emit()
# After network switching, one must make a new instance of Zeroconf
# On windows, the instance creation is very fast (unnoticable). Other platforms?
self._zero_conf = Zeroconf()
self._browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.', [self._appendServiceChangedRequest])
# Look for manual instances from preference
for address in self._manual_instances:
if address:
self.addManualPrinter(address)
def addManualPrinter(self, address):
if address not in self._manual_instances:
self._manual_instances.append(address)
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
instance_name = "manual:%s" % address
properties = {
b"name": address.encode("utf-8"),
b"address": address.encode("utf-8"),
b"manual": b"true",
b"incomplete": b"true"
}
if instance_name not in self._printers:
# Add a preliminary printer instance
self.addPrinter(instance_name, address, properties)
self.checkManualPrinter(address)
self.checkClusterPrinter(address)
def removeManualPrinter(self, key, address = None):
if key in self._printers:
if not address:
address = self._printers[key].ipAddress
self.removePrinter(key)
if address in self._manual_instances:
self._manual_instances.remove(address)
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
def checkManualPrinter(self, address):
# Check if a printer exists at this address
# If a printer responds, it will replace the preliminary printer created above
# origin=manual is for tracking back the origin of the call
url = QUrl("http://" + address + self._api_prefix + "system?origin=manual_name")
name_request = QNetworkRequest(url)
self._network_manager.get(name_request)
def checkClusterPrinter(self, address):
cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/?origin=check_cluster")
cluster_request = QNetworkRequest(cluster_url)
self._network_manager.get(cluster_request)
## Handler for all requests that have finished.
def _onNetworkRequestFinished(self, reply):
reply_url = reply.url().toString()
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if reply.operation() == QNetworkAccessManager.GetOperation:
address = reply.url().host()
if "origin=manual_name" in reply_url: # Name returned from printer.
if status_code == 200:
try:
system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.JSONDecodeError:
Logger.log("e", "Printer returned invalid JSON.")
return
except UnicodeDecodeError:
Logger.log("e", "Printer returned incorrect UTF-8.")
return
if address not in self._network_requests_buffer:
self._network_requests_buffer[address] = {}
self._network_requests_buffer[address]["system"] = system_info
elif "origin=check_cluster" in reply_url:
if address not in self._network_requests_buffer:
self._network_requests_buffer[address] = {}
if status_code == 200:
# We know it's a cluster printer
Logger.log("d", "Cluster printer detected: [%s]", reply.url())
try:
cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
except json.JSONDecodeError:
Logger.log("e", "Printer returned invalid JSON.")
return
except UnicodeDecodeError:
Logger.log("e", "Printer returned incorrect UTF-8.")
return
self._network_requests_buffer[address]["cluster"] = True
self._network_requests_buffer[address]["cluster_size"] = len(cluster_printers_list)
else:
Logger.log("d", "This url is not from a cluster printer: [%s]", reply.url())
self._network_requests_buffer[address]["cluster"] = False
# Both the system call and cluster call are finished
if (address in self._network_requests_buffer and
"system" in self._network_requests_buffer[address] and
"cluster" in self._network_requests_buffer[address]):
instance_name = "manual:%s" % address
system_info = self._network_requests_buffer[address]["system"]
machine = "unknown"
if "variant" in system_info:
variant = system_info["variant"]
if variant == "Ultimaker 3":
machine = "9066"
elif variant == "Ultimaker 3 Extended":
machine = "9511"
properties = {
b"name": system_info["name"].encode("utf-8"),
b"address": address.encode("utf-8"),
b"firmware_version": system_info["firmware"].encode("utf-8"),
b"manual": b"true",
b"machine": machine.encode("utf-8")
}
if self._network_requests_buffer[address]["cluster"]:
properties[b"cluster_size"] = self._network_requests_buffer[address]["cluster_size"]
if instance_name in self._printers:
# Only replace the printer if it is still in the list of (manual) printers
self.removePrinter(instance_name)
self.addPrinter(instance_name, address, properties)
del self._network_requests_buffer[address]
## Stop looking for devices on network.
def stop(self):
if self._zero_conf is not None:
Logger.log("d", "zeroconf close...")
self._zero_conf.close()
def getPrinters(self):
return self._printers
def reCheckConnections(self):
active_machine = Application.getInstance().getGlobalContainerStack()
if not active_machine:
return
for key in self._printers:
if key == active_machine.getMetaDataEntry("um_network_key"):
if not self._printers[key].isConnected():
Logger.log("d", "Connecting [%s]..." % key)
self._printers[key].connect()
self._printers[key].connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
else:
if self._printers[key].isConnected():
Logger.log("d", "Closing connection [%s]..." % key)
self._printers[key].close()
self._printers[key].connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
def addPrinter(self, name, address, properties):
cluster_size = int(properties.get(b"cluster_size", -1))
if cluster_size >= 0:
printer = NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice(
name, address, properties, self._api_prefix)
else:
printer = NetworkPrinterOutputDevice.NetworkPrinterOutputDevice(name, address, properties, self._api_prefix)
self._printers[printer.getKey()] = printer
self._cluster_printers_seen[printer.getKey()] = name # Cluster printers that may be temporary unreachable or is rebooted keep being stored here
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack and printer.getKey() == global_container_stack.getMetaDataEntry("um_network_key"):
if printer.getKey() not in self._old_printers: # Was the printer already connected, but a re-scan forced?
Logger.log("d", "addPrinter, connecting [%s]..." % printer.getKey())
self._printers[printer.getKey()].connect()
printer.connectionStateChanged.connect(self._onPrinterConnectionStateChanged)
self.printerListChanged.emit()
def removePrinter(self, name):
printer = self._printers.pop(name, None)
if printer:
if printer.isConnected():
printer.disconnect()
printer.connectionStateChanged.disconnect(self._onPrinterConnectionStateChanged)
Logger.log("d", "removePrinter, disconnecting [%s]..." % name)
self.printerListChanged.emit()
## Handler for when the connection state of one of the detected printers changes
def _onPrinterConnectionStateChanged(self, key):
if key not in self._printers:
return
if self._printers[key].isConnected():
self.getOutputDeviceManager().addOutputDevice(self._printers[key])
else:
self.getOutputDeviceManager().removeOutputDevice(key)
## Handler for zeroConf detection.
# Return True or False indicating if the process succeeded.
def _onServiceChanged(self, zeroconf, service_type, name, state_change):
if state_change == ServiceStateChange.Added:
Logger.log("d", "Bonjour service added: %s" % name)
# First try getting info from zeroconf cache
info = ServiceInfo(service_type, name, properties = {})
for record in zeroconf.cache.entries_with_name(name.lower()):
info.update_record(zeroconf, time.time(), record)
for record in zeroconf.cache.entries_with_name(info.server):
info.update_record(zeroconf, time.time(), record)
if info.address:
break
# Request more data if info is not complete
if not info.address:
Logger.log("d", "Trying to get address of %s", name)
info = zeroconf.get_service_info(service_type, name)
if info:
type_of_device = info.properties.get(b"type", None)
if type_of_device:
if type_of_device == b"printer":
address = '.'.join(map(lambda n: str(n), info.address))
if address in self._excluded_addresses:
Logger.log("d", "The IP address %s of the printer \'%s\' is not correct. Trying to reconnect.", address, name)
return False # When getting the localhost IP, then try to reconnect
self.addPrinterSignal.emit(str(name), address, info.properties)
else:
Logger.log("w", "The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device )
else:
Logger.log("w", "Could not get information about %s" % name)
return False
elif state_change == ServiceStateChange.Removed:
Logger.log("d", "Bonjour service removed: %s" % name)
self.removePrinterSignal.emit(str(name))
return True
## Appends a service changed request so later the handling thread will pick it up and processes it.
def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
# append the request and set the event so the event handling thread can pick it up
item = (zeroconf, service_type, name, state_change)
self._service_changed_request_queue.put(item)
self._service_changed_request_event.set()
def _handleOnServiceChangedRequests(self):
while True:
# wait for the event to be set
self._service_changed_request_event.wait(timeout = 5.0)
# stop if the application is shutting down
if Application.getInstance().isShuttingDown():
return
self._service_changed_request_event.clear()
# handle all pending requests
reschedule_requests = [] # a list of requests that have failed so later they will get re-scheduled
while not self._service_changed_request_queue.empty():
request = self._service_changed_request_queue.get()
zeroconf, service_type, name, state_change = request
try:
result = self._onServiceChanged(zeroconf, service_type, name, state_change)
if not result:
reschedule_requests.append(request)
except Exception:
Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled",
service_type, name)
reschedule_requests.append(request)
# re-schedule the failed requests if any
if reschedule_requests:
for request in reschedule_requests:
self._service_changed_request_queue.put(request)
@pyqtSlot()
def openControlPanel(self):
Logger.log("d", "Opening print jobs web UI...")
selected_device = self.getOutputDeviceManager().getActiveDevice()
if isinstance(selected_device, NetworkClusterPrinterOutputDevice.NetworkClusterPrinterOutputDevice):
QDesktopServices.openUrl(QUrl(selected_device.getPrintJobsUrl()))

View file

@ -10,12 +10,12 @@ Item
id: extruderInfo
property var printCoreConfiguration
width: Math.floor(parent.width / 2)
width: Math.round(parent.width / 2)
height: childrenRect.height
Label
{
id: materialLabel
text: printCoreConfiguration.material.material + " (" + printCoreConfiguration.material.color + ")"
text: printCoreConfiguration.activeMaterial != null ? printCoreConfiguration.activeMaterial.name : ""
elide: Text.ElideRight
width: parent.width
font: UM.Theme.getFont("very_small")
@ -23,7 +23,7 @@ Item
Label
{
id: printCoreLabel
text: printCoreConfiguration.print_core_id
text: printCoreConfiguration.hotendID
anchors.top: materialLabel.bottom
elide: Text.ElideRight
width: parent.width

View file

@ -20,8 +20,25 @@ UM.Dialog
visible: true
modality: Qt.ApplicationModal
onVisibleChanged:
{
if(visible)
{
resetPrintersModel()
}
}
title: catalog.i18nc("@title:window", "Print over network")
title: catalog.i18nc("@title:window","Print over network")
property var printersModel: ListModel{}
function resetPrintersModel() {
printersModel.clear()
printersModel.append({ name: "Automatic", key: ""})
for (var index in OutputDevice.printers)
{
printersModel.append({name: OutputDevice.printers[index].name, key: OutputDevice.printers[index].key})
}
}
Column
{
@ -31,8 +48,7 @@ UM.Dialog
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.rightMargin: UM.Theme.getSize("default_margin").width
height: 50 * screenScaleFactor
height: 50 * screenScaleFactord
Label
{
id: manualPrinterSelectionLabel
@ -42,7 +58,7 @@ UM.Dialog
topMargin: UM.Theme.getSize("default_margin").height
right: parent.right
}
text: "Printer selection"
text: catalog.i18nc("@label", "Printer selection")
wrapMode: Text.Wrap
height: 20 * screenScaleFactor
}
@ -50,18 +66,12 @@ UM.Dialog
ComboBox
{
id: printerSelectionCombobox
model: OutputDevice.printers
textRole: "friendly_name"
model: base.printersModel
textRole: "name"
width: parent.width
height: 40 * screenScaleFactor
Behavior on height { NumberAnimation { duration: 100 } }
onActivated:
{
var printerData = OutputDevice.printers[index];
OutputDevice.selectPrinter(printerData.unique_name, printerData.friendly_name);
}
}
SystemPalette
@ -79,8 +89,6 @@ UM.Dialog
enabled: true
onClicked: {
base.visible = false;
// reset to defaults
OutputDevice.selectAutomaticPrinter()
printerSelectionCombobox.currentIndex = 0
}
}
@ -93,9 +101,8 @@ UM.Dialog
enabled: true
onClicked: {
base.visible = false;
OutputDevice.sendPrintJob();
OutputDevice.sendPrintJob(printerSelectionCombobox.model.get(printerSelectionCombobox.currentIndex).key)
// reset to defaults
OutputDevice.selectAutomaticPrinter()
printerSelectionCombobox.currentIndex = 0
}
}

View file

@ -22,16 +22,16 @@ Rectangle
{
return "";
}
if (printJob.time_total === 0)
if (printJob.timeTotal === 0)
{
return "";
}
return Math.min(100, Math.round(printJob.time_elapsed / printJob.time_total * 100)) + "%";
return Math.min(100, Math.round(printJob.timeElapsed / printJob.timeTotal * 100)) + "%";
}
function printerStatusText(printer)
{
switch (printer.status)
switch (printer.state)
{
case "pre_print":
return catalog.i18nc("@label", "Preparing to print")
@ -49,31 +49,23 @@ Rectangle
}
id: printerDelegate
property var printer
property var printer: null
property var printJob: printer != null ? printer.activePrintJob: null
border.width: UM.Theme.getSize("default_lining").width
border.color: mouse.containsMouse ? emphasisColor : lineColor
z: mouse.containsMouse ? 1 : 0 // Push this item up a bit on mouse over to ensure that the highlighted bottom border is visible.
property var printJob:
{
if (printer.reserved_by != null)
{
// Look in another list.
return OutputDevice.printJobsByUUID[printer.reserved_by]
}
return OutputDevice.printJobsByPrinterUUID[printer.uuid]
}
MouseArea
{
id: mouse
anchors.fill:parent
onClicked: OutputDevice.selectPrinter(printer.unique_name, printer.friendly_name)
onClicked: OutputDevice.setActivePrinter(printer)
hoverEnabled: true;
// Only clickable if no printer is selected
enabled: OutputDevice.selectedPrinterName == "" && printer.status !== "unreachable"
enabled: OutputDevice.activePrinter == null && printer.state !== "unreachable"
}
Row
@ -86,7 +78,7 @@ Rectangle
Rectangle
{
width: Math.floor(parent.width / 3)
width: Math.round(parent.width / 3)
height: parent.height
Label // Print job name
@ -122,7 +114,7 @@ Rectangle
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: printJob != null ? getPrettyTime(printJob.time_total) : ""
text: printJob != null ? getPrettyTime(printJob.timeTotal) : ""
opacity: 0.65
font: UM.Theme.getFont("default")
elide: Text.ElideRight
@ -131,7 +123,7 @@ Rectangle
Rectangle
{
width: Math.floor(parent.width / 3 * 2)
width: Math.round(parent.width / 3 * 2)
height: parent.height
Label // Friendly machine name
@ -139,8 +131,8 @@ Rectangle
id: printerNameLabel
anchors.top: parent.top
anchors.left: parent.left
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width)
text: printer.friendly_name
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width)
text: printer.name
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
}
@ -149,8 +141,8 @@ Rectangle
{
id: printerTypeLabel
anchors.top: printerNameLabel.bottom
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
text: printer.machine_variant
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
text: printer.type
anchors.left: parent.left
elide: Text.ElideRight
font: UM.Theme.getFont("very_small")
@ -166,7 +158,7 @@ Rectangle
anchors.right: printProgressArea.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
color: emphasisColor
opacity: printer != null && printer.status === "unreachable" ? 0.3 : 1
opacity: printer != null && printer.state === "unreachable" ? 0.3 : 1
Image
{
@ -183,7 +175,7 @@ Rectangle
id: extruderInfo
anchors.bottom: parent.bottom
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
@ -191,8 +183,8 @@ Rectangle
PrintCoreConfiguration
{
id: leftExtruderInfo
width: Math.floor((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.configuration[0]
width: Math.round((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.extruders[0]
}
Rectangle
@ -206,8 +198,8 @@ Rectangle
PrintCoreConfiguration
{
id: rightExtruderInfo
width: Math.floor((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.configuration[1]
width: Math.round((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.extruders[1]
}
}
@ -217,7 +209,7 @@ Rectangle
anchors.right: parent.right
anchors.top: parent.top
height: showExtended ? parent.height: printProgressTitleBar.height
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
border.width: UM.Theme.getSize("default_lining").width
border.color: lineColor
radius: cornerRadius
@ -225,9 +217,9 @@ Rectangle
if(printJob != null)
{
var extendStates = ["sent_to_printer", "wait_for_configuration", "printing", "pre_print", "post_print", "wait_cleanup", "queued"];
return extendStates.indexOf(printJob.status) !== -1;
return extendStates.indexOf(printJob.state) !== -1;
}
return !printer.enabled;
return printer.state == "disabled"
}
Item // Status and Percent
@ -235,7 +227,7 @@ Rectangle
id: printProgressTitleBar
property var showPercent: {
return printJob != null && (["printing", "post_print", "pre_print", "sent_to_printer"].indexOf(printJob.status) !== -1);
return printJob != null && (["printing", "post_print", "pre_print", "sent_to_printer"].indexOf(printJob.state) !== -1);
}
width: parent.width
@ -252,19 +244,19 @@ Rectangle
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
text: {
if (!printer.enabled)
if (printer.state == "disabled")
{
return catalog.i18nc("@label:status", "Disabled");
}
if (printer.status === "unreachable")
if (printer.state === "unreachable")
{
return printerStatusText(printer);
}
if (printJob != null)
{
switch (printJob.status)
switch (printJob.state)
{
case "printing":
case "post_print":
@ -272,19 +264,13 @@ Rectangle
case "wait_for_configuration":
return catalog.i18nc("@label:status", "Reserved")
case "wait_cleanup":
case "wait_user_action":
return catalog.i18nc("@label:status", "Finished")
case "pre_print":
case "sent_to_printer":
return catalog.i18nc("@label", "Preparing to print")
case "queued":
if (printJob.configuration_changes_required != null && printJob.configuration_changes_required.length !== 0)
{
return catalog.i18nc("@label:status", "Action required");
}
else
{
return "";
}
case "pausing":
case "paused":
return catalog.i18nc("@label:status", "Paused");
@ -293,6 +279,7 @@ Rectangle
case "aborted":
return catalog.i18nc("@label:status", "Print aborted");
default:
// If print job has unknown status show printer.status
return printerStatusText(printer);
}
}
@ -328,26 +315,23 @@ Rectangle
visible: !printProgressTitleBar.showPercent
source: {
if (!printer.enabled)
if (printer.state == "disabled")
{
return "blocked-icon.svg";
}
if (printer.status === "unreachable")
if (printer.state === "unreachable")
{
return "";
}
if (printJob != null)
{
if(printJob.status === "queued")
if(printJob.state === "queued")
{
if (printJob.configuration_changes_required != null && printJob.configuration_changes_required.length !== 0)
{
return "action-required-icon.svg";
}
return "action-required-icon.svg";
}
else if (printJob.status === "wait_cleanup")
else if (printJob.state === "wait_cleanup")
{
return "checkmark-icon.svg";
}
@ -384,23 +368,23 @@ Rectangle
{
text:
{
if (!printer.enabled)
if (printer.state == "disabled")
{
return catalog.i18nc("@label", "Not accepting print jobs");
}
if (printer.status === "unreachable")
if (printer.state === "unreachable")
{
return "";
}
if(printJob != null)
{
switch (printJob.status)
switch (printJob.state)
{
case "printing":
case "post_print":
return catalog.i18nc("@label", "Finishes at: ") + OutputDevice.getTimeCompleted(printJob.time_total - printJob.time_elapsed)
return catalog.i18nc("@label", "Finishes at: ") + OutputDevice.getTimeCompleted(printJob.timeTotal - printJob.timeElapsed)
case "wait_cleanup":
return catalog.i18nc("@label", "Clear build plate")
case "sent_to_printer":
@ -409,10 +393,7 @@ Rectangle
case "wait_for_configuration":
return catalog.i18nc("@label", "Not accepting print jobs")
case "queued":
if (printJob.configuration_changes_required != undefined)
{
return catalog.i18nc("@label", "Waiting for configuration change");
}
return catalog.i18nc("@label", "Waiting for configuration change");
default:
return "";
}
@ -432,9 +413,9 @@ Rectangle
text: {
if(printJob != null)
{
if(printJob.status == "printing" || printJob.status == "post_print")
if(printJob.state == "printing" || printJob.state == "post_print")
{
return OutputDevice.getDateCompleted(printJob.time_total - printJob.time_elapsed)
return OutputDevice.getDateCompleted(printJob.timeTotal - printJob.timeElapsed)
}
}
return "";

View file

@ -17,7 +17,7 @@ Item
MouseArea
{
anchors.fill: parent
onClicked: OutputDevice.selectAutomaticPrinter()
onClicked: OutputDevice.setActivePrinter(null)
z: 0
}
@ -32,7 +32,7 @@ Item
width: 20 * screenScaleFactor
height: 20 * screenScaleFactor
onClicked: OutputDevice.selectAutomaticPrinter()
onClicked: OutputDevice.setActivePrinter(null)
style: ButtonStyle
{
@ -57,7 +57,7 @@ Item
{
id: cameraImage
width: Math.min(sourceSize.width === 0 ? 800 * screenScaleFactor : sourceSize.width, maximumWidth)
height: Math.floor((sourceSize.height === 0 ? 600 * screenScaleFactor : sourceSize.height) * width / sourceSize.width)
height: Math.round((sourceSize.height === 0 ? 600 * screenScaleFactor : sourceSize.height) * width / sourceSize.width)
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
z: 1
@ -65,17 +65,23 @@ Item
{
if(visible)
{
OutputDevice.startCamera()
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
{
OutputDevice.activePrinter.camera.start()
}
} else
{
OutputDevice.stopCamera()
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
{
OutputDevice.activePrinter.camera.stop()
}
}
}
source:
{
if(OutputDevice.cameraImage)
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage)
{
return OutputDevice.cameraImage;
return OutputDevice.activePrinter.camera.latestImage;
}
return "";
}

View file

@ -10,10 +10,11 @@ Item
{
id: base
property bool isUM3: Cura.MachineManager.activeQualityDefinitionId == "ultimaker3"
property string activeQualityDefinitionId: Cura.MachineManager.activeQualityDefinitionId
property bool isUM3: activeQualityDefinitionId == "ultimaker3" || activeQualityDefinitionId.match("ultimaker_") != null
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
property bool authenticationRequested: printerConnected && Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 // AuthState.AuthenticationRequested
property bool authenticationRequested: printerConnected && (Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 || Cura.MachineManager.printerOutputDevices[0].authenticationState == 5) // AuthState.AuthenticationRequested or AuthenticationReceived.
Row
{
@ -115,22 +116,8 @@ Item
{
tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura")
text: catalog.i18nc("@action:button", "Activate Configuration")
visible: printerConnected && !isClusterPrinter()
visible: false // printerConnected && !isClusterPrinter()
onClicked: manager.loadConfigurationFromPrinter()
function isClusterPrinter() {
if(Cura.MachineManager.printerOutputDevices.length == 0)
{
return false;
}
var clusterSize = Cura.MachineManager.printerOutputDevices[0].clusterSize;
// This is not a cluster printer or the cluster it is just one printer
if(clusterSize == undefined || clusterSize == 1)
{
return false;
}
return true;
}
}
}

View file

@ -0,0 +1,332 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from UM.Logger import Logger
from UM.Application import Application
from UM.Signal import Signal, signalemitter
from UM.Preferences import Preferences
from UM.Version import Version
from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice
from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
from PyQt5.QtCore import QUrl
from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
from queue import Queue
from threading import Event, Thread
from time import time
import json
## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
# Zero-Conf is used to detect printers, which are saved in a dict.
# If we discover a printer that has the same key as the active machine instance a connection is made.
@signalemitter
class UM3OutputDevicePlugin(OutputDevicePlugin):
addDeviceSignal = Signal()
removeDeviceSignal = Signal()
discoveredDevicesChanged = Signal()
def __init__(self):
super().__init__()
self._zero_conf = None
self._zero_conf_browser = None
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
self.addDeviceSignal.connect(self._onAddDevice)
self.removeDeviceSignal.connect(self._onRemoveDevice)
Application.getInstance().globalContainerStackChanged.connect(self.reCheckConnections)
self._discovered_devices = {}
self._network_manager = QNetworkAccessManager()
self._network_manager.finished.connect(self._onNetworkRequestFinished)
self._min_cluster_version = Version("4.0.0")
self._api_version = "1"
self._api_prefix = "/api/v" + self._api_version + "/"
self._cluster_api_version = "1"
self._cluster_api_prefix = "/cluster-api/v" + self._cluster_api_version + "/"
# Get list of manual instances from preferences
self._preferences = Preferences.getInstance()
self._preferences.addPreference("um3networkprinting/manual_instances",
"") # A comma-separated list of ip adresses or hostnames
self._manual_instances = self._preferences.getValue("um3networkprinting/manual_instances").split(",")
# The zero-conf service changed requests are handled in a separate thread, so we can re-schedule the requests
# which fail to get detailed service info.
# Any new or re-scheduled requests will be appended to the request queue, and the handling thread will pick
# them up and process them.
self._service_changed_request_queue = Queue()
self._service_changed_request_event = Event()
self._service_changed_request_thread = Thread(target=self._handleOnServiceChangedRequests, daemon=True)
self._service_changed_request_thread.start()
def getDiscoveredDevices(self):
return self._discovered_devices
## Start looking for devices on network.
def start(self):
self.startDiscovery()
def startDiscovery(self):
self.stop()
if self._zero_conf_browser:
self._zero_conf_browser.cancel()
self._zero_conf_browser = None # Force the old ServiceBrowser to be destroyed.
self._zero_conf = Zeroconf()
self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.',
[self._appendServiceChangedRequest])
# Look for manual instances from preference
for address in self._manual_instances:
if address:
self.addManualDevice(address)
def reCheckConnections(self):
active_machine = Application.getInstance().getGlobalContainerStack()
if not active_machine:
return
um_network_key = active_machine.getMetaDataEntry("um_network_key")
for key in self._discovered_devices:
if key == um_network_key:
if not self._discovered_devices[key].isConnected():
Logger.log("d", "Attempting to connect with [%s]" % key)
self._discovered_devices[key].connect()
self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
else:
if self._discovered_devices[key].isConnected():
Logger.log("d", "Attempting to close connection with [%s]" % key)
self._discovered_devices[key].close()
self._discovered_devices[key].connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
def _onDeviceConnectionStateChanged(self, key):
if key not in self._discovered_devices:
return
if self._discovered_devices[key].isConnected():
self.getOutputDeviceManager().addOutputDevice(self._discovered_devices[key])
else:
self.getOutputDeviceManager().removeOutputDevice(key)
def stop(self):
if self._zero_conf is not None:
Logger.log("d", "zeroconf close...")
self._zero_conf.close()
def removeManualDevice(self, key, address = None):
if key in self._discovered_devices:
if not address:
address = self._discovered_devices[key].ipAddress
self._onRemoveDevice(key)
if address in self._manual_instances:
self._manual_instances.remove(address)
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
def addManualDevice(self, address):
if address not in self._manual_instances:
self._manual_instances.append(address)
self._preferences.setValue("um3networkprinting/manual_instances", ",".join(self._manual_instances))
instance_name = "manual:%s" % address
properties = {
b"name": address.encode("utf-8"),
b"address": address.encode("utf-8"),
b"manual": b"true",
b"incomplete": b"true"
}
if instance_name not in self._discovered_devices:
# Add a preliminary printer instance
self._onAddDevice(instance_name, address, properties)
self._checkManualDevice(address)
def _checkManualDevice(self, address):
# Check if a UM3 family device exists at this address.
# If a printer responds, it will replace the preliminary printer created above
# origin=manual is for tracking back the origin of the call
url = QUrl("http://" + address + self._api_prefix + "system")
name_request = QNetworkRequest(url)
self._network_manager.get(name_request)
def _onNetworkRequestFinished(self, reply):
reply_url = reply.url().toString()
if "system" in reply_url:
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
# Something went wrong with checking the firmware version!
return
try:
system_info = json.loads(bytes(reply.readAll()).decode("utf-8"))
except:
Logger.log("e", "Something went wrong converting the JSON.")
return
address = reply.url().host()
has_cluster_capable_firmware = Version(system_info["firmware"]) > self._min_cluster_version
instance_name = "manual:%s" % address
properties = {
b"name": system_info["name"].encode("utf-8"),
b"address": address.encode("utf-8"),
b"firmware_version": system_info["firmware"].encode("utf-8"),
b"manual": b"true",
b"machine": system_info["variant"].encode("utf-8")
}
if has_cluster_capable_firmware:
# Cluster needs an additional request, before it's completed.
properties[b"incomplete"] = b"true"
# Check if the device is still in the list & re-add it with the updated
# information.
if instance_name in self._discovered_devices:
self._onRemoveDevice(instance_name)
self._onAddDevice(instance_name, address, properties)
if has_cluster_capable_firmware:
# We need to request more info in order to figure out the size of the cluster.
cluster_url = QUrl("http://" + address + self._cluster_api_prefix + "printers/")
cluster_request = QNetworkRequest(cluster_url)
self._network_manager.get(cluster_request)
elif "printers" in reply_url:
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
# Something went wrong with checking the amount of printers the cluster has!
return
# So we confirmed that the device is in fact a cluster printer, and we should now know how big it is.
try:
cluster_printers_list = json.loads(bytes(reply.readAll()).decode("utf-8"))
except:
Logger.log("e", "Something went wrong converting the JSON.")
return
address = reply.url().host()
instance_name = "manual:%s" % address
if instance_name in self._discovered_devices:
device = self._discovered_devices[instance_name]
properties = device.getProperties().copy()
if b"incomplete" in properties:
del properties[b"incomplete"]
properties[b'cluster_size'] = len(cluster_printers_list)
self._onRemoveDevice(instance_name)
self._onAddDevice(instance_name, address, properties)
def _onRemoveDevice(self, device_id):
device = self._discovered_devices.pop(device_id, None)
if device:
if device.isConnected():
device.disconnect()
try:
device.connectionStateChanged.disconnect(self._onDeviceConnectionStateChanged)
except TypeError:
# Disconnect already happened.
pass
self.discoveredDevicesChanged.emit()
def _onAddDevice(self, name, address, properties):
# Check what kind of device we need to add; Depending on the firmware we either add a "Connect"/"Cluster"
# or "Legacy" UM3 device.
cluster_size = int(properties.get(b"cluster_size", -1))
if cluster_size >= 0:
device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties)
else:
device = LegacyUM3OutputDevice.LegacyUM3OutputDevice(name, address, properties)
self._discovered_devices[device.getId()] = device
self.discoveredDevicesChanged.emit()
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"):
device.connect()
device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
## Appends a service changed request so later the handling thread will pick it up and processes it.
def _appendServiceChangedRequest(self, zeroconf, service_type, name, state_change):
# append the request and set the event so the event handling thread can pick it up
item = (zeroconf, service_type, name, state_change)
self._service_changed_request_queue.put(item)
self._service_changed_request_event.set()
def _handleOnServiceChangedRequests(self):
while True:
# Wait for the event to be set
self._service_changed_request_event.wait(timeout = 5.0)
# Stop if the application is shutting down
if Application.getInstance().isShuttingDown():
return
self._service_changed_request_event.clear()
# Handle all pending requests
reschedule_requests = [] # A list of requests that have failed so later they will get re-scheduled
while not self._service_changed_request_queue.empty():
request = self._service_changed_request_queue.get()
zeroconf, service_type, name, state_change = request
try:
result = self._onServiceChanged(zeroconf, service_type, name, state_change)
if not result:
reschedule_requests.append(request)
except Exception:
Logger.logException("e", "Failed to get service info for [%s] [%s], the request will be rescheduled",
service_type, name)
reschedule_requests.append(request)
# Re-schedule the failed requests if any
if reschedule_requests:
for request in reschedule_requests:
self._service_changed_request_queue.put(request)
## Handler for zeroConf detection.
# Return True or False indicating if the process succeeded.
# Note that this function can take over 3 seconds to complete. Be carefull calling it from the main thread.
def _onServiceChanged(self, zero_conf, service_type, name, state_change):
if state_change == ServiceStateChange.Added:
Logger.log("d", "Bonjour service added: %s" % name)
# First try getting info from zero-conf cache
info = ServiceInfo(service_type, name, properties={})
for record in zero_conf.cache.entries_with_name(name.lower()):
info.update_record(zero_conf, time(), record)
for record in zero_conf.cache.entries_with_name(info.server):
info.update_record(zero_conf, time(), record)
if info.address:
break
# Request more data if info is not complete
if not info.address:
Logger.log("d", "Trying to get address of %s", name)
info = zero_conf.get_service_info(service_type, name)
if info:
type_of_device = info.properties.get(b"type", None)
if type_of_device:
if type_of_device == b"printer":
address = '.'.join(map(lambda n: str(n), info.address))
self.addDeviceSignal.emit(str(name), address, info.properties)
else:
Logger.log("w",
"The type of the found device is '%s', not 'printer'! Ignoring.." % type_of_device)
else:
Logger.log("w", "Could not get information about %s" % name)
return False
elif state_change == ServiceStateChange.Removed:
Logger.log("d", "Bonjour service removed: %s" % name)
self.removeDeviceSignal.emit(str(name))
return True

View file

@ -1,12 +1,14 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import NetworkPrinterOutputDevicePlugin
from . import DiscoverUM3Action
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from . import UM3OutputDevicePlugin
def getMetaData():
return {}
def register(app):
return { "output_device": NetworkPrinterOutputDevicePlugin.NetworkPrinterOutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}
return { "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}

View file

@ -0,0 +1,66 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Job import Job
from UM.Logger import Logger
from .avr_isp.stk500v2 import Stk500v2
from time import time, sleep
from serial import Serial, SerialException
# An async job that attempts to find the correct baud rate for a USB printer.
# It tries a pre-set list of baud rates. All these baud rates are validated by requesting the temperature a few times
# and checking if the results make sense. If getResult() is not None, it was able to find a correct baud rate.
class AutoDetectBaudJob(Job):
def __init__(self, serial_port):
super().__init__()
self._serial_port = serial_port
self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600]
def run(self):
Logger.log("d", "Auto detect baud rate started.")
timeout = 3
programmer = Stk500v2()
serial = None
try:
programmer.connect(self._serial_port)
serial = programmer.leaveISP()
except:
programmer.close()
for baud_rate in self._all_baud_rates:
Logger.log("d", "Checking {serial} if baud rate {baud_rate} works".format(serial= self._serial_port, baud_rate = baud_rate))
if serial is None:
try:
serial = Serial(str(self._serial_port), baud_rate, timeout = timeout, writeTimeout = timeout)
except SerialException as e:
Logger.logException("w", "Unable to create serial")
continue
else:
# We already have a serial connection, just change the baud rate.
try:
serial.baudrate = baud_rate
except:
continue
sleep(1.5) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number
successful_responses = 0
serial.write(b"\n") # Ensure we clear out previous responses
serial.write(b"M105\n")
timeout_time = time() + timeout
while timeout_time > time():
line = serial.readline()
if b"ok T:" in line:
successful_responses += 1
if successful_responses >= 3:
self.setResult(baud_rate)
return
serial.write(b"M105\n")
self.setResult(None) # Unable to detect the correct baudrate.

View file

@ -1,4 +1,4 @@
// Copyright (c) 2015 Ultimaker B.V.
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
@ -34,44 +34,22 @@ UM.Dialog
}
text: {
if (manager.errorCode == 0)
switch (manager.firmwareUpdateState)
{
if (manager.firmwareUpdateCompleteStatus)
{
//: Firmware update status label
return catalog.i18nc("@label","Firmware update completed.")
}
else if (manager.progress == 0)
{
//: Firmware update status label
return catalog.i18nc("@label","Starting firmware update, this may take a while.")
}
else
{
//: Firmware update status label
case 0:
return "" //Not doing anything (eg; idling)
case 1:
return catalog.i18nc("@label","Updating firmware.")
}
}
else
{
switch (manager.errorCode)
{
case 1:
//: Firmware update status label
return catalog.i18nc("@label","Firmware update failed due to an unknown error.")
case 2:
//: Firmware update status label
return catalog.i18nc("@label","Firmware update failed due to an communication error.")
case 3:
//: Firmware update status label
return catalog.i18nc("@label","Firmware update failed due to an input/output error.")
case 4:
//: Firmware update status label
return catalog.i18nc("@label","Firmware update failed due to missing firmware.")
default:
//: Firmware update status label
return catalog.i18nc("@label", "Unknown error code: %1").arg(manager.errorCode)
}
case 2:
return catalog.i18nc("@label","Firmware update completed.")
case 3:
return catalog.i18nc("@label","Firmware update failed due to an unknown error.")
case 4:
return catalog.i18nc("@label","Firmware update failed due to an communication error.")
case 5:
return catalog.i18nc("@label","Firmware update failed due to an input/output error.")
case 6:
return catalog.i18nc("@label","Firmware update failed due to missing firmware.")
}
}
@ -81,16 +59,15 @@ UM.Dialog
ProgressBar
{
id: prog
value: manager.firmwareUpdateCompleteStatus ? 100 : manager.progress
value: manager.firmwareProgress
minimumValue: 0
maximumValue: 100
indeterminate: (manager.progress < 1) && (!manager.firmwareUpdateCompleteStatus)
indeterminate: manager.firmwareProgress < 1 && manager.firmwareProgress > 0
anchors
{
left: parent.left;
right: parent.right;
}
}
SystemPalette

View file

@ -0,0 +1,68 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from PyQt5.QtCore import QTimer
MYPY = False
if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
class USBPrinterOutputController(PrinterOutputController):
def __init__(self, output_device):
super().__init__(output_device)
self._preheat_bed_timer = QTimer()
self._preheat_bed_timer.setSingleShot(True)
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
self._preheat_printer = None
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
self._output_device.sendCommand("G91")
self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
self._output_device.sendCommand("G90")
def homeHead(self, printer):
self._output_device.sendCommand("G28 X")
self._output_device.sendCommand("G28 Y")
def homeBed(self, printer):
self._output_device.sendCommand("G28 Z")
def setJobState(self, job: "PrintJobOutputModel", state: str):
if state == "pause":
self._output_device.pausePrint()
job.updateState("paused")
elif state == "print":
self._output_device.resumePrint()
job.updateState("printing")
elif state == "abort":
self._output_device.cancelPrint()
pass
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
try:
temperature = round(temperature) # The API doesn't allow floating point.
duration = round(duration)
except ValueError:
return # Got invalid values, can't pre-heat.
self.setTargetBedTemperature(printer, temperature=temperature)
self._preheat_bed_timer.setInterval(duration * 1000)
self._preheat_bed_timer.start()
self._preheat_printer = printer
printer.updateIsPreheating(True)
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
self.preheatBed(printer, temperature=0, duration=0)
self._preheat_bed_timer.stop()
printer.updateIsPreheating(False)
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
self._output_device.sendCommand("M140 S%s" % temperature)
def _onPreheatBedTimerFinished(self):
self.setTargetBedTemperature(self._preheat_printer, 0)
self._preheat_printer.updateIsPreheating(False)

File diff suppressed because it is too large Load diff

View file

@ -2,33 +2,32 @@
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Signal import Signal, signalemitter
from . import USBPrinterOutputDevice
from UM.Application import Application
from UM.Resources import Resources
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from cura.PrinterOutputDevice import ConnectionState
from UM.Qt.ListModel import ListModel
from UM.Message import Message
from UM.i18n import i18nCatalog
from cura.PrinterOutputDevice import ConnectionState
from cura.CuraApplication import CuraApplication
from . import USBPrinterOutputDevice
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
import threading
import platform
import time
import os.path
import serial.tools.list_ports
from UM.Extension import Extension
from PyQt5.QtCore import QUrl, QObject, pyqtSlot, pyqtProperty, pyqtSignal, Qt
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
## Manager class that ensures that a usbPrinteroutput device is created for every connected USB printer.
## Manager class that ensures that an USBPrinterOutput device is created for every connected USB printer.
@signalemitter
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
addUSBOutputDeviceSignal = Signal()
progressChanged = pyqtSignal()
def __init__(self, parent = None):
super().__init__(parent = parent)
self._serial_port_list = []
@ -38,39 +37,10 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
self._update_thread.setDaemon(True)
self._check_updates = True
self._firmware_view = None
Application.getInstance().applicationShuttingDown.connect(self.stop)
self.addUSBOutputDeviceSignal.connect(self.addOutputDevice) #Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
addUSBOutputDeviceSignal = Signal()
connectionStateChanged = pyqtSignal()
progressChanged = pyqtSignal()
firmwareUpdateChange = pyqtSignal()
@pyqtProperty(float, notify = progressChanged)
def progress(self):
progress = 0
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
progress += device.progress
return progress / len(self._usb_output_devices)
@pyqtProperty(int, notify = progressChanged)
def errorCode(self):
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
if device._error_code:
return device._error_code
return 0
## Return True if all printers finished firmware update
@pyqtProperty(float, notify = firmwareUpdateChange)
def firmwareUpdateCompleteStatus(self):
complete = True
for printer_name, device in self._usb_output_devices.items(): # TODO: @UnusedVariable "printer_name"
if not device.firmwareUpdateFinished:
complete = False
return complete
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
self.addUSBOutputDeviceSignal.connect(self.addOutputDevice)
def start(self):
self._check_updates = True
@ -79,58 +49,28 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
def stop(self):
self._check_updates = False
def _updateThread(self):
while self._check_updates:
result = self.getSerialPortList(only_list_usb = True)
self._addRemovePorts(result)
time.sleep(5)
## Show firmware interface.
# This will create the view if its not already created.
def spawnFirmwareInterface(self, serial_port):
if self._firmware_view is None:
path = os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml")
self._firmware_view = Application.getInstance().createQmlComponent(path, {"manager": self})
self._firmware_view.show()
@pyqtSlot(str)
def updateAllFirmware(self, file_name):
if file_name.startswith("file://"):
file_name = QUrl(file_name).toLocalFile() # File dialogs prepend the path with file://, which we don't need / want
if not self._usb_output_devices:
Message(i18n_catalog.i18nc("@info", "Unable to update firmware because there are no printers connected."), title = i18n_catalog.i18nc("@info:title", "Warning")).show()
def _onConnectionStateChanged(self, serial_port):
if serial_port not in self._usb_output_devices:
return
for printer_connection in self._usb_output_devices:
self._usb_output_devices[printer_connection].resetFirmwareUpdate()
self.spawnFirmwareInterface("")
for printer_connection in self._usb_output_devices:
try:
self._usb_output_devices[printer_connection].updateFirmware(file_name)
except FileNotFoundError:
# Should only happen in dev environments where the resources/firmware folder is absent.
self._usb_output_devices[printer_connection].setProgress(100, 100)
Logger.log("w", "No firmware found for printer %s called '%s'", printer_connection, file_name)
Message(i18n_catalog.i18nc("@info",
"Could not find firmware required for the printer at %s.") % printer_connection, title = i18n_catalog.i18nc("@info:title", "Printer Firmware")).show()
self._firmware_view.close()
changed_device = self._usb_output_devices[serial_port]
if changed_device.connectionState == ConnectionState.connected:
self.getOutputDeviceManager().addOutputDevice(changed_device)
else:
self.getOutputDeviceManager().removeOutputDevice(serial_port)
def _updateThread(self):
while self._check_updates:
container_stack = Application.getInstance().getGlobalContainerStack()
if container_stack is None:
time.sleep(5)
continue
@pyqtSlot(str, str, result = bool)
def updateFirmwareBySerial(self, serial_port, file_name):
if serial_port in self._usb_output_devices:
self.spawnFirmwareInterface(self._usb_output_devices[serial_port].getSerialPort())
try:
self._usb_output_devices[serial_port].updateFirmware(file_name)
except FileNotFoundError:
self._firmware_view.close()
Logger.log("e", "Could not find firmware required for this machine called '%s'", file_name)
return False
return True
return False
if container_stack.getMetaDataEntry("supports_usb_connection"):
port_list = self.getSerialPortList(only_list_usb=True)
else:
port_list = [] # Just use an empty list; all USB devices will be removed.
self._addRemovePorts(port_list)
time.sleep(5)
## Return the singleton instance of the USBPrinterManager
@classmethod
@ -191,7 +131,11 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
Logger.log("w", "There is no firmware for machine %s.", machine_id)
if hex_file:
return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
try:
return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
except FileNotFoundError:
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
return ""
else:
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
return ""
@ -205,46 +149,16 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin, Extension):
continue
self._serial_port_list = list(serial_ports)
devices_to_remove = []
for port, device in self._usb_output_devices.items():
if port not in self._serial_port_list:
device.close()
devices_to_remove.append(port)
for port in devices_to_remove:
del self._usb_output_devices[port]
## Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
def addOutputDevice(self, serial_port):
device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port)
device.connectionStateChanged.connect(self._onConnectionStateChanged)
device.connect()
device.progressChanged.connect(self.progressChanged)
device.firmwareUpdateChange.connect(self.firmwareUpdateChange)
self._usb_output_devices[serial_port] = device
## If one of the states of the connected devices change, we might need to add / remove them from the global list.
def _onConnectionStateChanged(self, serial_port):
success = True
try:
if self._usb_output_devices[serial_port].connectionState == ConnectionState.connected:
self.getOutputDeviceManager().addOutputDevice(self._usb_output_devices[serial_port])
else:
success = success and self.getOutputDeviceManager().removeOutputDevice(serial_port)
if success:
self.connectionStateChanged.emit()
except KeyError:
Logger.log("w", "Connection state of %s changed, but it was not found in the list")
@pyqtProperty(QObject , notify = connectionStateChanged)
def connectedPrinterList(self):
self._usb_output_devices_model = ListModel()
self._usb_output_devices_model.addRoleName(Qt.UserRole + 1, "name")
self._usb_output_devices_model.addRoleName(Qt.UserRole + 2, "printer")
for connection in self._usb_output_devices:
if self._usb_output_devices[connection].connectionState == ConnectionState.connected:
self._usb_output_devices_model.appendItem({"name": connection, "printer": self._usb_output_devices[connection]})
return self._usb_output_devices_model
device.connect()
## Create a list of serial ports on the system.
# \param only_list_usb If true, only usb ports are listed

View file

@ -1,17 +1,18 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import USBPrinterOutputDeviceManager
from PyQt5.QtQml import qmlRegisterType, qmlRegisterSingletonType
from PyQt5.QtQml import qmlRegisterSingletonType
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
}
return {}
def register(app):
# We are violating the QT API here (as we use a factory, which is technically not allowed).
# but we don't really have another means for doing this (and it seems to you know -work-)
qmlRegisterSingletonType(USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager, "Cura", 1, 0, "USBPrinterManager", USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance)
return {"extension":USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance(), "output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance()}
return {"output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance()}

View file

@ -1,8 +1,7 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher.
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.InstanceContainer import InstanceContainer
from cura.MachineAction import MachineAction
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
@ -11,8 +10,6 @@ from UM.Application import Application
from UM.Util import parseBool
catalog = i18nCatalog("cura")
import UM.Settings.InstanceContainer
## The Ultimaker 2 can have a few revisions & upgrades.
class UM2UpgradeSelection(MachineAction):
@ -22,18 +19,28 @@ class UM2UpgradeSelection(MachineAction):
self._container_registry = ContainerRegistry.getInstance()
self._current_global_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._reset()
def _reset(self):
self.hasVariantsChanged.emit()
def _onGlobalStackChanged(self):
if self._current_global_stack:
self._current_global_stack.metaDataChanged.disconnect(self._onGlobalStackMetaDataChanged)
self._current_global_stack = Application.getInstance().getGlobalContainerStack()
if self._current_global_stack:
self._current_global_stack.metaDataChanged.connect(self._onGlobalStackMetaDataChanged)
self._reset()
def _onGlobalStackMetaDataChanged(self):
self._reset()
hasVariantsChanged = pyqtSignal()
@pyqtProperty(bool, notify = hasVariantsChanged)
def hasVariants(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
return parseBool(global_container_stack.getMetaDataEntry("has_variants", "false"))
@pyqtSlot(bool)
def setHasVariants(self, has_variants = True):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
@ -62,3 +69,9 @@ class UM2UpgradeSelection(MachineAction):
global_container_stack.extruders["0"].variant = ContainerRegistry.getInstance().getEmptyInstanceContainer()
Application.getInstance().globalContainerStackChanged.emit()
self._reset()
@pyqtProperty(bool, fset = setHasVariants, notify = hasVariantsChanged)
def hasVariants(self):
if self._current_global_stack:
return parseBool(self._current_global_stack.getMetaDataEntry("has_variants", "false"))

View file

@ -13,6 +13,7 @@ import Cura 1.0 as Cura
Cura.MachineAction
{
anchors.fill: parent;
Item
{
id: upgradeSelectionMachineAction
@ -39,12 +40,19 @@ Cura.MachineAction
CheckBox
{
id: olssonBlockCheckBox
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@label", "Olsson Block")
checked: manager.hasVariants
onClicked: manager.setHasVariants(checked)
onClicked: manager.hasVariants = checked
Connections
{
target: manager
onHasVariantsChanged: olssonBlockCheckBox.checked = manager.hasVariants
}
}
UM.I18nCatalog { id: catalog; name: "cura"; }

View file

@ -180,7 +180,7 @@ Cura.MachineAction
height: childrenRect.height
anchors.top: nozzleTempLabel.top
anchors.left: bedTempStatus.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width/2
anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width/2)
visible: checkupMachineAction.usbConnected
Button
{
@ -241,7 +241,7 @@ Cura.MachineAction
height: childrenRect.height
anchors.top: bedTempLabel.top
anchors.left: bedTempStatus.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width/2
anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width/2)
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed
Button
{

View file

@ -14,6 +14,9 @@ import Cura 1.0 as Cura
Cura.MachineAction
{
anchors.fill: parent;
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
property var activeOutputDevice: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null
Item
{
id: upgradeFirmwareMachineAction
@ -60,16 +63,17 @@ Cura.MachineAction
{
id: autoUpgradeButton
text: catalog.i18nc("@action:button", "Automatically upgrade Firmware");
enabled: parent.firmwareName != ""
enabled: parent.firmwareName != "" && activeOutputDevice
onClicked:
{
Cura.USBPrinterManager.updateAllFirmware(parent.firmwareName)
activeOutputDevice.updateFirmware(parent.firmwareName)
}
}
Button
{
id: manualUpgradeButton
text: catalog.i18nc("@action:button", "Upload custom Firmware");
enabled: activeOutputDevice != null
onClicked:
{
customFirmwareDialog.open()
@ -83,7 +87,7 @@ Cura.MachineAction
title: catalog.i18nc("@title:window", "Select custom firmware")
nameFilters: "Firmware image files (*.hex)"
selectExisting: true
onAccepted: Cura.USBPrinterManager.updateAllFirmware(fileUrl)
onAccepted: activeOutputDevice.updateFirmware(fileUrl)
}
}
}

View file

@ -3,7 +3,6 @@
from . import BedLevelMachineAction
from . import UpgradeFirmwareMachineAction
from . import UMOCheckupMachineAction
from . import UMOUpgradeSelection
from . import UM2UpgradeSelection
@ -18,7 +17,6 @@ def register(app):
return { "machine_action": [
BedLevelMachineAction.BedLevelMachineAction(),
UpgradeFirmwareMachineAction.UpgradeFirmwareMachineAction(),
UMOCheckupMachineAction.UMOCheckupMachineAction(),
UMOUpgradeSelection.UMOUpgradeSelection(),
UM2UpgradeSelection.UM2UpgradeSelection()
]}

View file

@ -9,8 +9,8 @@ import UM 1.3 as UM
UM.Dialog
{
id: baseDialog
minimumWidth: Math.floor(UM.Theme.getSize("modal_window_minimum").width * 0.75)
minimumHeight: Math.floor(UM.Theme.getSize("modal_window_minimum").height * 0.5)
minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width * 0.75)
minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height * 0.5)
width: minimumWidth
height: minimumHeight
title: catalog.i18nc("@title:window", "User Agreement")

View file

@ -74,7 +74,7 @@ class VersionUpgrade22to24(VersionUpgrade):
def __convertVariant(self, variant_path):
# Copy the variant to the machine_instances/*_settings.inst.cfg
variant_config = configparser.ConfigParser(interpolation=None)
with open(variant_path, "r") as fhandle:
with open(variant_path, "r", encoding = "utf-8") as fhandle:
variant_config.read_file(fhandle)
config_name = "Unknown Variant"

View file

@ -3,14 +3,9 @@
import configparser #To parse preference files.
import io #To serialise the preference files afterwards.
import os
from urllib.parse import quote_plus
from UM.Resources import Resources
from UM.VersionUpgrade import VersionUpgrade #We're inheriting from this.
from cura.CuraApplication import CuraApplication
# a list of all legacy "Not Supported" quality profiles
_OLD_NOT_SUPPORTED_PROFILES = [
@ -59,6 +54,12 @@ _EMPTY_CONTAINER_DICT = {
}
# Renamed definition files
_RENAMED_DEFINITION_DICT = {
"jellybox": "imade3d_jellybox",
}
class VersionUpgrade30to31(VersionUpgrade):
## Gets the version number from a CFG file in Uranium's 3.0 format.
#
@ -111,31 +112,9 @@ class VersionUpgrade30to31(VersionUpgrade):
if not parser.has_section(each_section):
parser.add_section(each_section)
# Copy global quality changes to extruder quality changes for single extrusion machines
if parser["metadata"]["type"] == "quality_changes":
all_quality_changes = self._getSingleExtrusionMachineQualityChanges(parser)
# Note that DO NOT!!! use the quality_changes returned from _getSingleExtrusionMachineQualityChanges().
# Those are loaded from the hard drive which are original files that haven't been upgraded yet.
# NOTE 2: The number can be 0 or 1 depends on whether you are loading it from the qualities folder or
# from a project file. When you load from a project file, the custom profile may not be in cura
# yet, so you will get 0.
if len(all_quality_changes) <= 1 and not parser.has_option("metadata", "extruder"):
self._createExtruderQualityChangesForSingleExtrusionMachine(filename, parser)
if parser["metadata"]["type"] == "definition_changes":
if parser["general"]["definition"] == "custom":
# We are only interested in machine_nozzle_size
if parser.has_option("values", "machine_nozzle_size"):
machine_nozzle_size = parser["values"]["machine_nozzle_size"]
definition_name = parser["general"]["name"]
machine_extruders = self._getSingleExtrusionMachineExtruders(definition_name)
#For single extuder machine we nee only first extruder
if len(machine_extruders) !=0:
if self._updateSingleExtuderDefinitionFile(machine_extruders, machine_nozzle_size):
parser.remove_option("values", "machine_nozzle_size")
# Check renamed definitions
if "definition" in parser["general"] and parser["general"]["definition"] in _RENAMED_DEFINITION_DICT:
parser["general"]["definition"] = _RENAMED_DEFINITION_DICT[parser["general"]["definition"]]
# Update version numbers
parser["general"]["version"] = "2"
@ -146,7 +125,6 @@ class VersionUpgrade30to31(VersionUpgrade):
parser.write(output)
return [filename], [output.getvalue()]
## Upgrades a container stack from version 3.0 to 3.1.
#
# \param serialised The serialised form of a container stack.
@ -171,6 +149,10 @@ class VersionUpgrade30to31(VersionUpgrade):
if parser.has_option("containers", key) and parser["containers"][key] == "empty":
parser["containers"][key] = specific_empty_container
# check renamed definition
if parser.has_option("containers", "6") and parser["containers"]["6"] in _RENAMED_DEFINITION_DICT:
parser["containers"]["6"] = _RENAMED_DEFINITION_DICT[parser["containers"]["6"]]
# Update version numbers
if "general" not in parser:
parser["general"] = {}
@ -184,194 +166,3 @@ class VersionUpgrade30to31(VersionUpgrade):
output = io.StringIO()
parser.write(output)
return [filename], [output.getvalue()]
def _getSingleExtrusionMachineQualityChanges(self, quality_changes_container):
quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
quality_changes_containers = []
for item in os.listdir(quality_changes_dir):
file_path = os.path.join(quality_changes_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation = None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("metadata", "type"):
continue
if "quality_changes" != parser["metadata"]["type"]:
continue
if not parser.has_option("general", "name"):
continue
if quality_changes_container["general"]["name"] != parser["general"]["name"]:
continue
quality_changes_containers.append(parser)
return quality_changes_containers
def _getSingleExtrusionMachineExtruders(self, definition_name):
machine_instances_dir = Resources.getPath(CuraApplication.ResourceTypes.MachineStack)
machine_instances = []
#Find all machine instances
for item in os.listdir(machine_instances_dir):
file_path = os.path.join(machine_instances_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("metadata", "type"):
continue
if "machine" != parser["metadata"]["type"]:
continue
if not parser.has_option("general", "id"):
continue
machine_instances.append(parser)
#Find for extruders
extruders_instances_dir = Resources.getPath(CuraApplication.ResourceTypes.ExtruderStack)
#"machine",[extruders]
extruder_instances_per_machine = {}
#Find all custom extruders for founded machines
for item in os.listdir(extruders_instances_dir):
file_path = os.path.join(extruders_instances_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("metadata", "type"):
continue
if "extruder_train" != parser["metadata"]["type"]:
continue
if not parser.has_option("metadata", "machine"):
continue
if not parser.has_option("metadata", "position"):
continue
for machine_instace in machine_instances:
machine_id = machine_instace["general"]["id"]
if machine_id != parser["metadata"]["machine"]:
continue
if machine_id + "_settings" != definition_name:
continue
if extruder_instances_per_machine.get(machine_id) is None:
extruder_instances_per_machine.update({machine_id:[]})
extruder_instances_per_machine.get(machine_id).append(parser)
#the extruder can be related only to one machine
break
return extruder_instances_per_machine
#Find extruder defition at index 0 and update its values
def _updateSingleExtuderDefinitionFile(self, extruder_instances_per_machine, machine_nozzle_size):
defintion_instances_dir = Resources.getPath(CuraApplication.ResourceTypes.DefinitionChangesContainer)
for item in os.listdir(defintion_instances_dir):
file_path = os.path.join(defintion_instances_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation=None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("general", "name"):
continue
name = parser["general"]["name"]
custom_extruder_at_0_position = None
for machine_extruders in extruder_instances_per_machine:
for extruder_instance in extruder_instances_per_machine[machine_extruders]:
if extruder_instance["general"]["id"] + "_settings" == name:
defition_position = extruder_instance["metadata"]["position"]
if defition_position == "0":
custom_extruder_at_0_position = extruder_instance
break
if custom_extruder_at_0_position is not None:
break
#If not null, then parsed file is for first extuder and then can be updated. I need to update only
# first, because this update for single extuder machine
if custom_extruder_at_0_position is not None:
#Add new value
parser["values"]["machine_nozzle_size"] = machine_nozzle_size
definition_output = io.StringIO()
parser.write(definition_output)
with open(file_path, "w") as f:
f.write(definition_output.getvalue())
return True
return False
def _createExtruderQualityChangesForSingleExtrusionMachine(self, filename, global_quality_changes):
suffix = "_" + quote_plus(global_quality_changes["general"]["name"].lower())
machine_name = os.path.os.path.basename(filename).replace(".inst.cfg", "").replace(suffix, "")
# Why is this here?!
# When we load a .curaprofile file the deserialize will trigger a version upgrade, creating a dangling file.
# This file can be recognized by it's lack of a machine name in the target filename.
# So when we detect that situation here, we don't create the file and return.
if machine_name == "":
return
new_filename = machine_name + "_" + "fdmextruder" + suffix
extruder_quality_changes_parser = configparser.ConfigParser(interpolation = None)
extruder_quality_changes_parser.add_section("general")
extruder_quality_changes_parser["general"]["version"] = str(2)
extruder_quality_changes_parser["general"]["name"] = global_quality_changes["general"]["name"]
extruder_quality_changes_parser["general"]["definition"] = global_quality_changes["general"]["definition"]
extruder_quality_changes_parser.add_section("metadata")
extruder_quality_changes_parser["metadata"]["quality_type"] = global_quality_changes["metadata"]["quality_type"]
extruder_quality_changes_parser["metadata"]["type"] = global_quality_changes["metadata"]["type"]
extruder_quality_changes_parser["metadata"]["setting_version"] = str(4)
extruder_quality_changes_parser["metadata"]["extruder"] = "fdmextruder"
extruder_quality_changes_output = io.StringIO()
extruder_quality_changes_parser.write(extruder_quality_changes_output)
extruder_quality_changes_filename = quote_plus(new_filename) + ".inst.cfg"
quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
with open(os.path.join(quality_changes_dir, extruder_quality_changes_filename), "w") as f:
f.write(extruder_quality_changes_output.getvalue())

Some files were not shown because too many files have changed in this diff Show more