mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-12-11 16:00:47 -07:00
Merge branch 'master' into feature_xmlmaterials_cura_settings
# Conflicts: # plugins/XmlMaterialProfile/XmlMaterialProfile.py
This commit is contained in:
commit
0578e74e71
840 changed files with 56973 additions and 26870 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -390,6 +390,13 @@ UM.Dialog
|
|||
}
|
||||
}
|
||||
|
||||
function accept() {
|
||||
manager.closeBackend();
|
||||
manager.onOkButtonClicked();
|
||||
base.visible = false;
|
||||
base.accept();
|
||||
}
|
||||
|
||||
function reject() {
|
||||
manager.onCancelButtonClicked();
|
||||
base.visible = false;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 model’s 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 system’s 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.
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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 []
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"]]);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
474
plugins/PluginBrowser/PluginEntry.qml
Normal file
474
plugins/PluginBrowser/PluginEntry.qml
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
209
plugins/PostProcessingPlugin/PostProcessingPlugin.py
Normal file
209
plugins/PostProcessingPlugin/PostProcessingPlugin.py
Normal 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")
|
||||
|
||||
|
||||
501
plugins/PostProcessingPlugin/PostProcessingPlugin.qml
Normal file
501
plugins/PostProcessingPlugin/PostProcessingPlugin.qml
Normal 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{ }
|
||||
}
|
||||
}
|
||||
}
|
||||
2
plugins/PostProcessingPlugin/README.md
Normal file
2
plugins/PostProcessingPlugin/README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# PostProcessingPlugin
|
||||
A post processing plugin for Cura
|
||||
163
plugins/PostProcessingPlugin/Script.py
Normal file
163
plugins/PostProcessingPlugin/Script.py
Normal 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()
|
||||
11
plugins/PostProcessingPlugin/__init__.py
Normal file
11
plugins/PostProcessingPlugin/__init__.py
Normal 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()}
|
||||
8
plugins/PostProcessingPlugin/plugin.json
Normal file
8
plugins/PostProcessingPlugin/plugin.json
Normal 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"
|
||||
}
|
||||
47
plugins/PostProcessingPlugin/postprocessing.svg
Normal file
47
plugins/PostProcessingPlugin/postprocessing.svg
Normal 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 |
48
plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py
Normal file
48
plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py
Normal 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
|
||||
495
plugins/PostProcessingPlugin/scripts/ChangeAtZ.py
Normal file
495
plugins/PostProcessingPlugin/scripts/ChangeAtZ.py
Normal 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
|
||||
76
plugins/PostProcessingPlugin/scripts/ColorChange.py
Normal file
76
plugins/PostProcessingPlugin/scripts/ColorChange.py
Normal 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
|
||||
43
plugins/PostProcessingPlugin/scripts/ExampleScript.py
Normal file
43
plugins/PostProcessingPlugin/scripts/ExampleScript.py
Normal 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
|
||||
275
plugins/PostProcessingPlugin/scripts/PauseAtHeight.py
Normal file
275
plugins/PostProcessingPlugin/scripts/PauseAtHeight.py
Normal 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
|
||||
169
plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py
Normal file
169
plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py
Normal 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
|
||||
56
plugins/PostProcessingPlugin/scripts/SearchAndReplace.py
Normal file
56
plugins/PostProcessingPlugin/scripts/SearchAndReplace.py
Normal 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
|
||||
469
plugins/PostProcessingPlugin/scripts/Stretch.py
Normal file
469
plugins/PostProcessingPlugin/scripts/Stretch.py
Normal 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)
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
71
plugins/SupportEraser/SupportEraser.py
Normal file
71
plugins/SupportEraser/SupportEraser.py
Normal 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)
|
||||
20
plugins/SupportEraser/__init__.py
Normal file
20
plugins/SupportEraser/__init__.py
Normal 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() }
|
||||
8
plugins/SupportEraser/plugin.json
Normal file
8
plugins/SupportEraser/plugin.json
Normal 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"
|
||||
}
|
||||
11
plugins/SupportEraser/tool_icon.svg
Normal file
11
plugins/SupportEraser/tool_icon.svg
Normal 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 |
56
plugins/UFPWriter/UFPWriter.py
Normal file
56
plugins/UFPWriter/UFPWriter.py
Normal 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
|
||||
38
plugins/UFPWriter/__init__.py
Normal file
38
plugins/UFPWriter/__init__.py
Normal 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() }
|
||||
BIN
plugins/UFPWriter/kitten.png
Normal file
BIN
plugins/UFPWriter/kitten.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 443 KiB |
8
plugins/UFPWriter/plugin.json
Normal file
8
plugins/UFPWriter/plugin.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
453
plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py
Normal file
453
plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
636
plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py
Normal file
636
plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -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 "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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()))
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 "";
|
||||
|
|
|
|||
|
|
@ -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 "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
332
plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py
Normal file
332
plugins/UM3NetworkPrinting/UM3OutputDevicePlugin.py
Normal 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
|
||||
|
|
@ -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()}
|
||||
66
plugins/USBPrinting/AutoDetectBaudJob.py
Normal file
66
plugins/USBPrinting/AutoDetectBaudJob.py
Normal 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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
68
plugins/USBPrinting/USBPrinterOutputController.py
Normal file
68
plugins/USBPrinting/USBPrinterOutputController.py
Normal 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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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"; }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue