Merge remote-tracking branch 'origin/master' into feature_send_material_profiles

This commit is contained in:
Ian Paschal 2018-07-02 12:37:56 +02:00
commit 8f7370db6c
703 changed files with 272749 additions and 117038 deletions

View file

@ -1,6 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
import os.path
import zipfile
@ -37,8 +38,8 @@ except ImportError:
## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!
class ThreeMFReader(MeshReader):
def __init__(self, application):
super().__init__(application)
def __init__(self) -> None:
super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
@ -168,6 +169,8 @@ class ThreeMFReader(MeshReader):
archive = zipfile.ZipFile(file_name, "r")
self._base_name = os.path.basename(file_name)
parser = Savitar.ThreeMFParser()
with open("/tmp/test.xml", "wb") as f:
f.write(archive.open("3D/3dmodel.model").read())
scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read())
self._unit = scene_3mf.getUnit()
for node in scene_3mf.getSceneNodes():
@ -198,9 +201,9 @@ class ThreeMFReader(MeshReader):
# Second step: 3MF defines the left corner of the machine as center, whereas cura uses the center of the
# build volume.
if global_container_stack:
translation_vector = Vector(x=-global_container_stack.getProperty("machine_width", "value") / 2,
y=-global_container_stack.getProperty("machine_depth", "value") / 2,
z=0)
translation_vector = Vector(x = -global_container_stack.getProperty("machine_width", "value") / 2,
y = -global_container_stack.getProperty("machine_depth", "value") / 2,
z = 0)
translation_matrix = Matrix()
translation_matrix.setByTranslation(translation_vector)
transformation_matrix.multiply(translation_matrix)
@ -236,23 +239,20 @@ class ThreeMFReader(MeshReader):
# * inch
# * foot
# * meter
def _getScaleFromUnit(self, unit):
def _getScaleFromUnit(self, unit: Optional[str]) -> Vector:
conversion_to_mm = {
"micron": 0.001,
"millimeter": 1,
"centimeter": 10,
"meter": 1000,
"inch": 25.4,
"foot": 304.8
}
if unit is None:
unit = "millimeter"
if unit == "micron":
scale = 0.001
elif unit == "millimeter":
scale = 1
elif unit == "centimeter":
scale = 10
elif unit == "inch":
scale = 25.4
elif unit == "foot":
scale = 304.8
elif unit == "meter":
scale = 1000
else:
Logger.log("w", "Unrecognised unit %s used. Assuming mm instead", unit)
scale = 1
elif unit not in conversion_to_mm:
Logger.log("w", "Unrecognised unit {unit} used. Assuming mm instead.".format(unit = unit))
unit = "millimeter"
return Vector(scale, scale, scale)
scale = conversion_to_mm[unit]
return Vector(scale, scale, scale)

View file

@ -4,7 +4,7 @@
from configparser import ConfigParser
import zipfile
import os
from typing import List, Tuple
from typing import Dict, List, Tuple
import xml.etree.ElementTree as ET
@ -38,7 +38,7 @@ i18n_catalog = i18nCatalog("cura")
class ContainerInfo:
def __init__(self, file_name: str, serialized: str, parser: ConfigParser):
def __init__(self, file_name: str, serialized: str, parser: ConfigParser) -> None:
self.file_name = file_name
self.serialized = serialized
self.parser = parser
@ -47,14 +47,14 @@ class ContainerInfo:
class QualityChangesInfo:
def __init__(self):
def __init__(self) -> None:
self.name = None
self.global_info = None
self.extruder_info_dict = {}
self.extruder_info_dict = {} # type: Dict[str, ContainerInfo]
class MachineInfo:
def __init__(self):
def __init__(self) -> None:
self.container_id = None
self.name = None
self.definition_id = None
@ -66,11 +66,11 @@ class MachineInfo:
self.definition_changes_info = None
self.user_changes_info = None
self.extruder_info_dict = {}
self.extruder_info_dict = {} # type: Dict[str, ExtruderInfo]
class ExtruderInfo:
def __init__(self):
def __init__(self) -> None:
self.position = None
self.enabled = True
self.variant_info = None
@ -82,7 +82,7 @@ class ExtruderInfo:
## Base implementation for reading 3MF workspace files.
class ThreeMFWorkspaceReader(WorkspaceReader):
def __init__(self):
def __init__(self) -> None:
super().__init__()
MimeTypeDatabase.addMimeType(
@ -112,28 +112,26 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# - variant
self._ignored_instance_container_types = {"quality", "variant"}
self._resolve_strategies = {}
self._resolve_strategies = {} # type: Dict[str, str]
self._id_mapping = {}
self._id_mapping = {} # type: Dict[str, str]
# In Cura 2.5 and 2.6, the empty profiles used to have those long names
self._old_empty_profile_id_dict = {"empty_%s" % k: "empty" for k in ["material", "variant"]}
self._is_same_machine_type = False
self._old_new_materials = {}
self._materials_to_select = {}
self._old_new_materials = {} # type: Dict[str, str]
self._machine_info = None
def _clearState(self):
self._is_same_machine_type = False
self._id_mapping = {}
self._old_new_materials = {}
self._materials_to_select = {}
self._machine_info = None
## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results.
# This has nothing to do with speed, but with getting consistent new naming for instances & objects.
def getNewId(self, old_id):
def getNewId(self, old_id: str):
if old_id not in self._id_mapping:
self._id_mapping[old_id] = self._container_registry.uniqueName(old_id)
return self._id_mapping[old_id]
@ -671,7 +669,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
else:
material_container = materials[0]
old_material_root_id = material_container.getMetaDataEntry("base_file")
if not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only.
if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only.
to_deserialize_material = True
if self._resolve_strategies["material"] == "override":

View file

@ -187,7 +187,10 @@ class WorkspaceDialog(QObject):
@pyqtProperty(int, constant = True)
def totalNumberOfSettings(self):
return len(ContainerRegistry.getInstance().findDefinitionContainers(id="fdmprinter")[0].getAllKeys())
general_definition_containers = ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")
if not general_definition_containers:
return 0
return len(general_definition_containers[0].getAllKeys())
@pyqtProperty(int, notify = numVisibleSettingsChanged)
def numVisibleSettings(self):

View file

@ -18,7 +18,7 @@ catalog = i18nCatalog("cura")
def getMetaData() -> Dict:
# Workarround for osx not supporting double file extensions correctly.
# Workaround for osx not supporting double file extensions correctly.
if Platform.isOSX():
workspace_extension = "3mf"
else:
@ -44,7 +44,7 @@ def getMetaData() -> Dict:
def register(app):
if "3MFReader.ThreeMFReader" in sys.modules:
return {"mesh_reader": ThreeMFReader.ThreeMFReader(app),
return {"mesh_reader": ThreeMFReader.ThreeMFReader(),
"workspace_reader": ThreeMFWorkspaceReader.ThreeMFWorkspaceReader()}
else:
return {}

View file

@ -91,7 +91,7 @@ class ThreeMFWriter(MeshWriter):
# Handle per object settings (if any)
stack = um_node.callDecoration("getStack")
if stack is not None:
changed_setting_keys = set(stack.getTop().getAllKeys())
changed_setting_keys = stack.getTop().getAllKeys()
# Ensure that we save the extruder used for this object in a multi-extrusion setup
if stack.getProperty("machine_extruder_count", "value") > 1:

View file

@ -1,8 +1,14 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from collections import defaultdict
import os
from PyQt5.QtCore import QObject, QTimer, pyqtSlot
import sys
from time import time
from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING
from UM.Backend.Backend import Backend, BackendState
from UM.Application import Application
from UM.Scene.SceneNode import SceneNode
from UM.Signal import Signal
from UM.Logger import Logger
@ -10,29 +16,30 @@ from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Platform import Platform
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Qt.Duration import DurationFormat
from PyQt5.QtCore import QObject, pyqtSlot
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.Interfaces import DefinitionContainerInterface
from UM.Settings.SettingInstance import SettingInstance #For typing.
from UM.Tool import Tool #For typing.
from collections import defaultdict
from cura.CuraApplication import CuraApplication
from cura.Settings.ExtruderManager import ExtruderManager
from . import ProcessSlicedLayersJob
from . import StartSliceJob
import os
import sys
from time import time
from PyQt5.QtCore import QTimer
from .ProcessSlicedLayersJob import ProcessSlicedLayersJob
from .StartSliceJob import StartSliceJob, StartJobResult
import Arcus
if TYPE_CHECKING:
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
from cura.Machines.MachineErrorChecker import MachineErrorChecker
from UM.Scene.Scene import Scene
from UM.Settings.ContainerStack import ContainerStack
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class CuraEngineBackend(QObject, Backend):
backendError = Signal()
## Starts the back-end plug-in.
@ -40,16 +47,16 @@ class CuraEngineBackend(QObject, Backend):
# This registers all the signal listeners and prepares for communication
# with the back-end in general.
# CuraEngineBackend is exposed to qml as well.
def __init__(self, parent = None):
super().__init__(parent = parent)
def __init__(self) -> None:
super().__init__()
# Find out where the engine is located, and how it is called.
# This depends on how Cura is packaged and which OS we are running on.
executable_name = "CuraEngine"
if Platform.isWindows():
executable_name += ".exe"
default_engine_location = executable_name
if os.path.exists(os.path.join(Application.getInstallPrefix(), "bin", executable_name)):
default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", executable_name)
if os.path.exists(os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)):
default_engine_location = os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)
if hasattr(sys, "frozen"):
default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name)
if Platform.isLinux() and not default_engine_location:
@ -61,9 +68,9 @@ class CuraEngineBackend(QObject, Backend):
default_engine_location = execpath
break
self._application = Application.getInstance()
self._multi_build_plate_model = None
self._machine_error_checker = None
self._application = CuraApplication.getInstance() #type: CuraApplication
self._multi_build_plate_model = None #type: MultiBuildPlateModel
self._machine_error_checker = None #type: MachineErrorChecker
if not default_engine_location:
raise EnvironmentError("Could not find CuraEngine")
@ -71,16 +78,16 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
default_engine_location = os.path.abspath(default_engine_location)
Application.getInstance().getPreferences().addPreference("backend/location", default_engine_location)
self._application.getPreferences().addPreference("backend/location", default_engine_location)
# Workaround to disable layer view processing if layer view is not active.
self._layer_view_active = False
self._layer_view_active = False #type: bool
self._onActiveViewChanged()
self._stored_layer_data = []
self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
self._stored_layer_data = [] #type: List[Arcus.PythonMessage]
self._stored_optimized_layer_data = {} #type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
self._scene = self._application.getController().getScene()
self._scene = self._application.getController().getScene() #type: Scene
self._scene.sceneChanged.connect(self._onSceneChanged)
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
@ -91,7 +98,7 @@ class CuraEngineBackend(QObject, Backend):
# If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished
# to start the auto-slicing timer again.
#
self._global_container_stack = None
self._global_container_stack = None #type: Optional[ContainerStack]
# Listeners for receiving messages from the back-end.
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
@ -102,39 +109,39 @@ class CuraEngineBackend(QObject, Backend):
self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
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._build_plates_to_be_sliced = [] # what needs slicing?
self._engine_is_fresh = True # Is the newly started engine used before or not?
self._start_slice_job = None #type: Optional[StartSliceJob]
self._start_slice_job_build_plate = None #type: Optional[int]
self._slicing = False #type: bool # Are we currently slicing?
self._restart = False #type: bool # Back-end is currently restarting?
self._tool_active = False #type: bool # If a tool is active, some tasks do not have to do anything
self._always_restart = True #type: bool # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness.
self._process_layers_job = None #type: Optional[ProcessSlicedLayersJob] # The currently active job to process layers, or None if it is not processing layers.
self._build_plates_to_be_sliced = [] #type: List[int] # what needs slicing?
self._engine_is_fresh = True #type: bool # 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 = 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._backend_log_max_lines = 20000 #type: int # Maximum number of lines to buffer
self._error_message = None #type: Message # Pop-up message that shows errors.
self._last_num_objects = defaultdict(int) #type: Dict[int, int] # Count number of objects to see if there is something changed
self._postponed_scene_change_sources = [] #type: List[SceneNode] # scene change is postponed (by a tool)
self._slice_start_time = None
self._is_disabled = False
self._slice_start_time = None #type: Optional[float]
self._is_disabled = False #type: bool
Application.getInstance().getPreferences().addPreference("general/auto_slice", False)
self._application.getPreferences().addPreference("general/auto_slice", False)
self._use_timer = False
self._use_timer = False #type: bool
# When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.
# This timer will group them up, and only slice for the last setting changed signal.
# TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction.
self._change_timer = QTimer()
self._change_timer = QTimer() #type: QTimer
self._change_timer.setSingleShot(True)
self._change_timer.setInterval(500)
self.determineAutoSlicing()
Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._application.initializationFinished.connect(self.initialize)
def initialize(self):
def initialize(self) -> None:
self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
self._application.getController().activeViewChanged.connect(self._onActiveViewChanged)
@ -160,16 +167,16 @@ class CuraEngineBackend(QObject, Backend):
#
# This function should terminate the engine process.
# Called when closing the application.
def close(self):
def close(self) -> None:
# Terminate CuraEngine if it is still running at this point
self._terminate()
## Get the command that is used to call the engine.
# This is useful for debugging and used to actually start the engine.
# \return list of commands and args / parameters.
def getEngineCommand(self):
def getEngineCommand(self) -> List[str]:
json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json")
return [Application.getInstance().getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
return [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
## Emitted when we get a message containing print duration and material amount.
# This also implies the slicing has finished.
@ -184,13 +191,13 @@ class CuraEngineBackend(QObject, Backend):
slicingCancelled = Signal()
@pyqtSlot()
def stopSlicing(self):
def stopSlicing(self) -> None:
self.backendStateChange.emit(BackendState.NotStarted)
if self._slicing: # We were already slicing. Stop the old job.
self._terminate()
self._createSocket()
if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon.
if self._process_layers_job is not None: # 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
@ -200,12 +207,12 @@ class CuraEngineBackend(QObject, Backend):
## Manually triggers a reslice
@pyqtSlot()
def forceSlice(self):
def forceSlice(self) -> None:
self.markSliceAll()
self.slice()
## Perform a slice of the scene.
def slice(self):
def slice(self) -> None:
Logger.log("d", "Starting to slice...")
self._slice_start_time = time()
if not self._build_plates_to_be_sliced:
@ -218,10 +225,10 @@ class CuraEngineBackend(QObject, Backend):
return
if not hasattr(self._scene, "gcode_dict"):
self._scene.gcode_dict = {}
self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here.
# see if we really have to slice
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
active_build_plate = self._application.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._numObjectsPerBuildPlate()
@ -230,14 +237,14 @@ class CuraEngineBackend(QObject, Backend):
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
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] = []
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #Because we created this attribute above.
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
if Application.getInstance().getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
Application.getInstance().getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
if self._process is None:
self._createSocket()
@ -247,14 +254,14 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(0.0)
self.backendStateChange.emit(BackendState.NotStarted)
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #[] indexed by build plate number
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #[] 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 = 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()
@ -262,7 +269,7 @@ class CuraEngineBackend(QObject, Backend):
## Terminate the engine process.
# Start the engine process by calling _createSocket()
def _terminate(self):
def _terminate(self) -> None:
self._slicing = False
self._stored_layer_data = []
if self._start_slice_job_build_plate in self._stored_optimized_layer_data:
@ -274,7 +281,7 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(0)
Logger.log("d", "Attempting to kill the engine process")
if Application.getInstance().getUseExternalBackend():
if self._application.getUseExternalBackend():
return
if self._process is not None:
@ -295,7 +302,7 @@ class CuraEngineBackend(QObject, Backend):
# bootstrapping of a slice job.
#
# \param job The start slice job that was just finished.
def _onStartSliceCompleted(self, job):
def _onStartSliceCompleted(self, job: StartSliceJob) -> None:
if self._error_message:
self._error_message.hide()
@ -303,13 +310,13 @@ class CuraEngineBackend(QObject, Backend):
if self._start_slice_job is job:
self._start_slice_job = None
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
if job.isCancelled() or job.getError() or job.getResult() == StartJobResult.Error:
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
return
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
if Application.getInstance().platformActivity:
if job.getResult() == StartJobResult.MaterialIncompatible:
if self._application.platformActivity:
self._error_message = Message(catalog.i18nc("@info:status",
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
@ -319,10 +326,10 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.NotStarted)
return
if job.getResult() == StartSliceJob.StartJobResult.SettingError:
if Application.getInstance().platformActivity:
if job.getResult() == StartJobResult.SettingError:
if self._application.platformActivity:
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
error_keys = []
error_keys = [] #type: List[str]
for extruder in extruders:
error_keys.extend(extruder.getErrorKeys())
if not extruders:
@ -330,7 +337,7 @@ class CuraEngineBackend(QObject, Backend):
error_labels = set()
for key in error_keys:
for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack.
definitions = stack.getBottom().findDefinitions(key = key)
definitions = cast(DefinitionContainerInterface, stack.getBottom()).findDefinitions(key = key)
if definitions:
break #Found it! No need to continue search.
else: #No stack has a definition for this setting.
@ -338,8 +345,7 @@ class CuraEngineBackend(QObject, Backend):
continue
error_labels.add(definitions[0].label)
error_labels = ", ".join(error_labels)
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(error_labels),
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(", ".join(error_labels)),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
@ -348,29 +354,27 @@ class CuraEngineBackend(QObject, Backend):
self.backendStateChange.emit(BackendState.NotStarted)
return
elif job.getResult() == StartSliceJob.StartJobResult.ObjectSettingError:
elif job.getResult() == StartJobResult.ObjectSettingError:
errors = {}
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
for node in DepthFirstIterator(self._application.getController().getScene().getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
stack = node.callDecoration("getStack")
if not stack:
continue
for key in stack.getErrorKeys():
definition = self._global_container_stack.getBottom().findDefinitions(key = key)
definition = cast(DefinitionContainerInterface, self._global_container_stack.getBottom()).findDefinitions(key = key)
if not definition:
Logger.log("e", "When checking settings for errors, unable to find definition for key {key} in per-object stack.".format(key = key))
continue
definition = definition[0]
errors[key] = definition.label
error_labels = ", ".join(errors.values())
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = error_labels),
errors[key] = definition[0].label
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = ", ".join(errors.values())),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
return
if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
if Application.getInstance().platformActivity:
if job.getResult() == StartJobResult.BuildPlateError:
if self._application.platformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
@ -379,7 +383,7 @@ class CuraEngineBackend(QObject, Backend):
else:
self.backendStateChange.emit(BackendState.NotStarted)
if job.getResult() == StartSliceJob.StartJobResult.ObjectsWithDisabledExtruder:
if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder:
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because there are objects associated with disabled Extruder %s." % job.getMessage()),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
@ -387,8 +391,8 @@ class CuraEngineBackend(QObject, Backend):
self.backendError.emit(job)
return
if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice:
if Application.getInstance().platformActivity:
if job.getResult() == StartJobResult.NothingToSlice:
if self._application.platformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
@ -411,20 +415,20 @@ class CuraEngineBackend(QObject, Backend):
# It disables when
# - preference auto slice is off
# - decorator isBlockSlicing is found (used in g-code reader)
def determineAutoSlicing(self):
def determineAutoSlicing(self) -> bool:
enable_timer = True
self._is_disabled = False
if not Application.getInstance().getPreferences().getValue("general/auto_slice"):
if not self._application.getPreferences().getValue("general/auto_slice"):
enable_timer = False
for node in DepthFirstIterator(self._scene.getRoot()):
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
if node.callDecoration("isBlockSlicing"):
enable_timer = False
self.backendStateChange.emit(BackendState.Disabled)
self._is_disabled = True
gcode_list = node.callDecoration("getGCodeList")
if gcode_list is not None:
self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list
self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list #type: ignore #Because we generate this attribute dynamically.
if self._use_timer == enable_timer:
return self._use_timer
@ -437,9 +441,9 @@ class CuraEngineBackend(QObject, Backend):
return False
## Return a dict with number of objects per build plate
def _numObjectsPerBuildPlate(self):
num_objects = defaultdict(int)
for node in DepthFirstIterator(self._scene.getRoot()):
def _numObjectsPerBuildPlate(self) -> Dict[int, int]:
num_objects = defaultdict(int) #type: Dict[int, int]
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
# Only count sliceable objects
if node.callDecoration("isSliceable"):
build_plate_number = node.callDecoration("getBuildPlateNumber")
@ -451,7 +455,7 @@ class CuraEngineBackend(QObject, Backend):
# 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):
def _onSceneChanged(self, source: SceneNode) -> None:
if not isinstance(source, SceneNode):
return
@ -506,8 +510,8 @@ class CuraEngineBackend(QObject, Backend):
## Called when an error occurs in the socket connection towards the engine.
#
# \param error The exception that occurred.
def _onSocketError(self, error):
if Application.getInstance().isShuttingDown():
def _onSocketError(self, error: Arcus.Error) -> None:
if self._application.isShuttingDown():
return
super()._onSocketError(error)
@ -521,19 +525,19 @@ 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, build_plate_numbers = set()):
for node in DepthFirstIterator(self._scene.getRoot()):
def _clearLayerData(self, build_plate_numbers: Set = None) -> None:
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
if node.callDecoration("getLayerData"):
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
node.getParent().removeChild(node)
def markSliceAll(self):
for build_plate_number in range(Application.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1):
def markSliceAll(self) -> None:
for build_plate_number in range(self._application.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):
def needsSlicing(self) -> None:
self.stopSlicing()
self.markSliceAll()
self.processingProgress.emit(0.0)
@ -545,7 +549,7 @@ class CuraEngineBackend(QObject, Backend):
## A setting has changed, so check if we must reslice.
# \param instance The setting instance that has changed.
# \param property The property of the setting instance that has changed.
def _onSettingChanged(self, instance, property):
def _onSettingChanged(self, instance: SettingInstance, property: str) -> None:
if property == "value": # Only reslice if the value has changed.
self.needsSlicing()
self._onChanged()
@ -554,7 +558,7 @@ class CuraEngineBackend(QObject, Backend):
if self._use_timer:
self._change_timer.stop()
def _onStackErrorCheckFinished(self):
def _onStackErrorCheckFinished(self) -> None:
self.determineAutoSlicing()
if self._is_disabled:
return
@ -566,13 +570,13 @@ class CuraEngineBackend(QObject, Backend):
## Called when a sliced layer data message is received from the engine.
#
# \param message The protobuf message containing sliced layer data.
def _onLayerMessage(self, message):
def _onLayerMessage(self, message: Arcus.PythonMessage) -> None:
self._stored_layer_data.append(message)
## Called when an optimized sliced layer data message is received from the engine.
#
# \param message The protobuf message containing sliced layer data.
def _onOptimizedLayerMessage(self, message):
def _onOptimizedLayerMessage(self, message: Arcus.PythonMessage) -> None:
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)
@ -580,11 +584,11 @@ class CuraEngineBackend(QObject, Backend):
## Called when a progress message is received from the engine.
#
# \param message The protobuf message containing the slicing progress.
def _onProgressMessage(self, message):
def _onProgressMessage(self, message: Arcus.PythonMessage) -> None:
self.processingProgress.emit(message.amount)
self.backendStateChange.emit(BackendState.Processing)
def _invokeSlice(self):
def _invokeSlice(self) -> None:
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
@ -600,17 +604,17 @@ class CuraEngineBackend(QObject, Backend):
## Called when the engine sends a message that slicing is finished.
#
# \param message The protobuf message signalling that slicing is finished.
def _onSlicingFinishedMessage(self, message):
def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None:
self.backendStateChange.emit(BackendState.Done)
self.processingProgress.emit(1.0)
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate]
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically.
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))
replaced = line.replace("{print_time}", str(self._application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
replaced = replaced.replace("{filament_amount}", str(self._application.getPrintInformation().materialLengths))
replaced = replaced.replace("{filament_weight}", str(self._application.getPrintInformation().materialWeights))
replaced = replaced.replace("{filament_cost}", str(self._application.getPrintInformation().materialCosts))
replaced = replaced.replace("{jobname}", str(self._application.getPrintInformation().jobName))
gcode_list[index] = replaced
@ -619,7 +623,7 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("d", "Number of models per buildplate: %s", dict(self._numObjectsPerBuildPlate()))
# See if we need to process the sliced layers job.
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate
if (
self._layer_view_active and
(self._process_layers_job is None or not self._process_layers_job.isRunning()) and
@ -641,25 +645,27 @@ class CuraEngineBackend(QObject, Backend):
## 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_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace"))
def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None:
self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically.
## 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_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace"))
def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None:
self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically.
## Creates a new socket connection.
def _createSocket(self):
super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")))
def _createSocket(self, protocol_file: str = None) -> None:
if not protocol_file:
protocol_file = os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto"))
super()._createSocket(protocol_file)
self._engine_is_fresh = True
## Called when anything has changed to the stuff that needs to be sliced.
#
# This indicates that we should probably re-slice soon.
def _onChanged(self, *args, **kwargs):
def _onChanged(self, *args: Any, **kwargs: Any) -> None:
self.needsSlicing()
if self._use_timer:
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
@ -677,7 +683,7 @@ class CuraEngineBackend(QObject, Backend):
#
# \param message The protobuf message containing the print time per feature and
# material amount per extruder
def _onPrintTimeMaterialEstimates(self, message):
def _onPrintTimeMaterialEstimates(self, message: Arcus.PythonMessage) -> None:
material_amounts = []
for index in range(message.repeatedMessageCount("materialEstimates")):
material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount)
@ -688,7 +694,7 @@ class CuraEngineBackend(QObject, Backend):
## Called for parsing message to retrieve estimated time per feature
#
# \param message The protobuf message containing the print time per feature
def _parseMessagePrintTimes(self, message):
def _parseMessagePrintTimes(self, message: Arcus.PythonMessage) -> Dict[str, float]:
result = {
"inset_0": message.time_inset_0,
"inset_x": message.time_inset_x,
@ -705,7 +711,7 @@ class CuraEngineBackend(QObject, Backend):
return result
## Called when the back-end connects to the front-end.
def _onBackendConnected(self):
def _onBackendConnected(self) -> None:
if self._restart:
self._restart = False
self._onChanged()
@ -716,7 +722,7 @@ class CuraEngineBackend(QObject, Backend):
# continuously slicing while the user is dragging some tool handle.
#
# \param tool The tool that the user is using.
def _onToolOperationStarted(self, tool):
def _onToolOperationStarted(self, tool: Tool) -> None:
self._tool_active = True # Do not react on scene change
self.disableTimer()
# Restart engine as soon as possible, we know we want to slice afterwards
@ -729,7 +735,7 @@ class CuraEngineBackend(QObject, Backend):
# This indicates that we can safely start slicing again.
#
# \param tool The tool that the user was using.
def _onToolOperationStopped(self, tool):
def _onToolOperationStopped(self, tool: Tool) -> None:
self._tool_active = False # React on scene change again
self.determineAutoSlicing() # Switch timer on if appropriate
# Process all the postponed scene changes
@ -737,18 +743,17 @@ 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])
def _startProcessSlicedLayersJob(self, build_plate_number: int) -> None:
self._process_layers_job = 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):
application = Application.getInstance()
view = application.getController().getActiveView()
def _onActiveViewChanged(self) -> None:
view = self._application.getController().getActiveView()
if view:
active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
active_build_plate = self._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
@ -766,14 +771,14 @@ class CuraEngineBackend(QObject, Backend):
## Called when the back-end self-terminates.
#
# We should reset our state and start listening for new connections.
def _onBackendQuit(self):
def _onBackendQuit(self) -> None:
if not self._restart:
if self._process:
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait())
self._process = None
## Called when the global container stack changes
def _onGlobalStackChanged(self):
def _onGlobalStackChanged(self) -> None:
if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged)
self._global_container_stack.containersChanged.disconnect(self._onChanged)
@ -783,7 +788,7 @@ class CuraEngineBackend(QObject, Backend):
extruder.propertyChanged.disconnect(self._onSettingChanged)
extruder.containersChanged.disconnect(self._onChanged)
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
self._global_container_stack = self._application.getGlobalContainerStack()
if self._global_container_stack:
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
@ -794,26 +799,26 @@ class CuraEngineBackend(QObject, Backend):
extruder.containersChanged.connect(self._onChanged)
self._onChanged()
def _onProcessLayersFinished(self, job):
def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None:
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):
def enableTimer(self) -> None:
if not self._use_timer:
self._change_timer.timeout.connect(self.slice)
self._use_timer = True
## Disconnect slice function from timer.
# This means that slicing will not be triggered automatically
def disableTimer(self):
def disableTimer(self) -> None:
if self._use_timer:
self._use_timer = False
self._change_timer.timeout.disconnect(self.slice)
def _onPreferencesChanged(self, preference):
def _onPreferencesChanged(self, preference: str) -> None:
if preference != "general/auto_slice":
return
auto_slice = self.determineAutoSlicing()
@ -821,11 +826,11 @@ class CuraEngineBackend(QObject, Backend):
self._change_timer.start()
## Tickle the backend so in case of auto slicing, it starts the timer.
def tickle(self):
def tickle(self) -> None:
if self._use_timer:
self._change_timer.start()
def _extruderChanged(self):
def _extruderChanged(self) -> None:
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number)

View file

@ -1,21 +1,25 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import numpy
from string import Formatter
from enum import IntEnum
import time
from typing import Any, Dict, List, Optional, Set
import re
import Arcus #For typing.
from UM.Job import Job
from UM.Application import Application
from UM.Logger import Logger
from UM.Settings.ContainerStack import ContainerStack #For typing.
from UM.Settings.SettingRelation import SettingRelation #For typing.
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.Scene import Scene #For typing.
from UM.Settings.Validator import ValidatorState
from UM.Settings.SettingRelation import RelationType
from cura.CuraApplication import CuraApplication
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.OneAtATimeIterator import OneAtATimeIterator
from cura.Settings.ExtruderManager import ExtruderManager
@ -35,19 +39,19 @@ class StartJobResult(IntEnum):
ObjectsWithDisabledExtruder = 8
## Formatter class that handles token expansion in start/end gcod
## Formatter class that handles token expansion in start/end gcode
class GcodeStartEndFormatter(Formatter):
def get_value(self, key, args, kwargs): # [CodeStyle: get_value is an overridden function from the Formatter class]
def get_value(self, key: str, *args: str, **kwargs) -> str: #type: ignore # [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:
extruder_nr = kwargs["default_extruder_nr"]
extruder_nr = int(kwargs["default_extruder_nr"])
except ValueError:
extruder_nr = -1
key_fragments = [fragment.strip() for fragment in key.split(',')]
key_fragments = [fragment.strip() for fragment in key.split(",")]
if len(key_fragments) == 2:
try:
extruder_nr = int(key_fragments[1])
@ -74,25 +78,25 @@ class GcodeStartEndFormatter(Formatter):
## Job class that builds up the message of scene data to send to CuraEngine.
class StartSliceJob(Job):
def __init__(self, slice_message):
def __init__(self, slice_message: Arcus.PythonMessage) -> None:
super().__init__()
self._scene = Application.getInstance().getController().getScene()
self._slice_message = slice_message
self._is_cancelled = False
self._build_plate_number = None
self._scene = CuraApplication.getInstance().getController().getScene() #type: Scene
self._slice_message = slice_message #type: Arcus.PythonMessage
self._is_cancelled = False #type: bool
self._build_plate_number = None #type: Optional[int]
self._all_extruders_settings = None # cache for all setting values from all stacks (global & extruder) for the current machine
self._all_extruders_settings = None #type: Optional[Dict[str, Any]] # cache for all setting values from all stacks (global & extruder) for the current machine
def getSliceMessage(self):
def getSliceMessage(self) -> Arcus.PythonMessage:
return self._slice_message
def setBuildPlate(self, build_plate_number):
def setBuildPlate(self, build_plate_number: int) -> None:
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):
def _checkStackForErrors(self, stack: ContainerStack) -> bool:
if stack is None:
return False
@ -105,28 +109,28 @@ class StartSliceJob(Job):
return False
## Runs the job that initiates the slicing.
def run(self):
def run(self) -> None:
if self._build_plate_number is None:
self.setResult(StartJobResult.Error)
return
stack = Application.getInstance().getGlobalContainerStack()
stack = CuraApplication.getInstance().getGlobalContainerStack()
if not stack:
self.setResult(StartJobResult.Error)
return
# Don't slice if there is a setting with an error value.
if Application.getInstance().getMachineManager().stacksHaveErrors:
if CuraApplication.getInstance().getMachineManager().stacksHaveErrors:
self.setResult(StartJobResult.SettingError)
return
if Application.getInstance().getBuildVolume().hasErrors():
if CuraApplication.getInstance().getBuildVolume().hasErrors():
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:
if not CuraApplication.getInstance().getMachineManager().variantBuildplateCompatible and \
not CuraApplication.getInstance().getMachineManager().variantBuildplateUsable:
self.setResult(StartJobResult.MaterialIncompatible)
return
@ -141,7 +145,7 @@ class StartSliceJob(Job):
# Don't slice if there is a per object setting with an error value.
for node in DepthFirstIterator(self._scene.getRoot()):
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
if not isinstance(node, CuraSceneNode) or not node.isSelectable():
continue
@ -151,7 +155,7 @@ class StartSliceJob(Job):
with self._scene.getSceneLock():
# Remove old layer data.
for node in DepthFirstIterator(self._scene.getRoot()):
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
node.getParent().removeChild(node)
break
@ -159,7 +163,7 @@ class StartSliceJob(Job):
# Get the objects in their groups to print.
object_groups = []
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
for node in OneAtATimeIterator(self._scene.getRoot()):
for node in OneAtATimeIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
temp_list = []
# Node can't be printed, so don't bother sending it.
@ -185,7 +189,7 @@ class StartSliceJob(Job):
else:
temp_list = []
has_printing_mesh = False
for node in DepthFirstIterator(self._scene.getRoot()):
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
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
@ -212,27 +216,26 @@ class StartSliceJob(Job):
if temp_list:
object_groups.append(temp_list)
extruders_enabled = {position: stack.isEnabled for position, stack in Application.getInstance().getGlobalContainerStack().extruders.items()}
extruders_enabled = {position: stack.isEnabled for position, stack in CuraApplication.getInstance().getGlobalContainerStack().extruders.items()}
filtered_object_groups = []
has_model_with_disabled_extruders = False
associated_siabled_extruders = set()
associated_disabled_extruders = set()
for group in object_groups:
stack = Application.getInstance().getGlobalContainerStack()
stack = CuraApplication.getInstance().getGlobalContainerStack()
skip_group = False
for node in group:
extruder_position = node.callDecoration("getActiveExtruderPosition")
if not extruders_enabled[extruder_position]:
skip_group = True
has_model_with_disabled_extruders = True
associated_siabled_extruders.add(extruder_position)
break
associated_disabled_extruders.add(extruder_position)
if not skip_group:
filtered_object_groups.append(group)
if has_model_with_disabled_extruders:
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
associated_siabled_extruders = [str(c) for c in sorted([int(p) + 1 for p in associated_siabled_extruders])]
self.setMessage(", ".join(associated_siabled_extruders))
associated_disabled_extruders = [str(c) for c in sorted([int(p) + 1 for p in associated_disabled_extruders])]
self.setMessage(", ".join(associated_disabled_extruders))
return
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
@ -284,11 +287,11 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.Finished)
def cancel(self):
def cancel(self) -> None:
super().cancel()
self._is_cancelled = True
def isCancelled(self):
def isCancelled(self) -> bool:
return self._is_cancelled
## Creates a dictionary of tokens to replace in g-code pieces.
@ -298,7 +301,7 @@ class StartSliceJob(Job):
# with.
# \return A dictionary of replacement tokens to the values they should be
# replaced with.
def _buildReplacementTokens(self, stack) -> dict:
def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]:
result = {}
for key in stack.getAllKeys():
value = stack.getProperty(key, "value")
@ -311,7 +314,7 @@ 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_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
result["initial_extruder_nr"] = initial_extruder_nr
@ -320,9 +323,9 @@ class StartSliceJob(Job):
## Replace setting tokens in a piece of g-code.
# \param value A piece of g-code to replace tokens in.
# \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):
def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1) -> str:
if not self._all_extruders_settings:
global_stack = Application.getInstance().getGlobalContainerStack()
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
# NB: keys must be strings for the string formatter
self._all_extruders_settings = {
@ -344,7 +347,7 @@ class StartSliceJob(Job):
return str(value)
## Create extruder message from stack
def _buildExtruderMessage(self, stack):
def _buildExtruderMessage(self, stack: ContainerStack) -> None:
message = self._slice_message.addRepeatedMessage("extruders")
message.id = int(stack.getMetaDataEntry("position"))
@ -371,7 +374,7 @@ class StartSliceJob(Job):
#
# The settings are taken from the global stack. This does not include any
# per-extruder settings or per-object settings.
def _buildGlobalSettingsMessage(self, stack):
def _buildGlobalSettingsMessage(self, stack: ContainerStack) -> None:
settings = self._buildReplacementTokens(stack)
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
@ -385,7 +388,7 @@ class StartSliceJob(Job):
# Replace the setting tokens in start and end g-code.
# 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_stack = CuraApplication.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)
@ -406,7 +409,7 @@ class StartSliceJob(Job):
#
# \param stack The global stack with all settings, from which to read the
# limit_to_extruder property.
def _buildGlobalInheritsStackMessage(self, stack):
def _buildGlobalInheritsStackMessage(self, stack: ContainerStack) -> None:
for key in stack.getAllKeys():
extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
if extruder_position >= 0: # Set to a specific extruder.
@ -416,9 +419,9 @@ class StartSliceJob(Job):
Job.yieldThread()
## Check if a node has per object settings and ensure that they are set correctly in the message
# \param node \type{SceneNode} Node to check.
# \param node Node to check.
# \param message object_lists message to put the per object settings in
def _handlePerObjectSettings(self, node, message):
def _handlePerObjectSettings(self, node: CuraSceneNode, message: Arcus.PythonMessage):
stack = node.callDecoration("getStack")
# Check if the node has a stack attached to it and the stack has any settings in the top container.
@ -427,7 +430,7 @@ class StartSliceJob(Job):
# Check all settings for relations, so we can also calculate the correct values for dependent settings.
top_of_stack = stack.getTop() # Cache for efficiency.
changed_setting_keys = set(top_of_stack.getAllKeys())
changed_setting_keys = top_of_stack.getAllKeys()
# Add all relations to changed settings as well.
for key in top_of_stack.getAllKeys():
@ -456,9 +459,9 @@ class StartSliceJob(Job):
Job.yieldThread()
## Recursive function to put all settings that require each other for value changes in a list
# \param relations_set \type{set} Set of keys (strings) of settings that are influenced
# \param relations_set Set of keys of settings that are influenced
# \param relations list of relation objects that need to be checked.
def _addRelations(self, relations_set, relations):
def _addRelations(self, relations_set: Set[str], relations: List[SettingRelation]):
for relation in filter(lambda r: r.role == "value" or r.role == "limit_to_extruder", relations):
if relation.type == RelationType.RequiresTarget:
continue

View file

@ -11,9 +11,8 @@ from UM.PluginRegistry import PluginRegistry
#
# If you're zipping g-code, you might as well use gzip!
class GCodeGzReader(MeshReader):
def __init__(self, application):
super().__init__(application)
def __init__(self) -> None:
super().__init__()
self._supported_extensions = [".gcode.gz"]
def _read(self, file_name):

View file

@ -19,6 +19,7 @@ def getMetaData():
]
}
def register(app):
app.addNonSliceableExtension(".gz")
return { "mesh_reader": GCodeGzReader.GCodeGzReader(app) }
return {"mesh_reader": GCodeGzReader.GCodeGzReader()}

View file

@ -3,7 +3,7 @@
import gzip
from io import StringIO, BufferedIOBase #To write the g-code to a temporary buffer, and for typing.
from typing import List
from typing import cast, List
from UM.Logger import Logger
from UM.Mesh.MeshWriter import MeshWriter #The class we're extending/implementing.
@ -32,7 +32,7 @@ class GCodeGzWriter(MeshWriter):
#Get the g-code from the g-code writer.
gcode_textio = StringIO() #We have to convert the g-code into bytes.
success = PluginRegistry.getInstance().getPluginObject("GCodeWriter").write(gcode_textio, None)
success = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter")).write(gcode_textio, None)
if not success: #Writing the g-code failed. Then I can also not write the gzipped g-code.
return False

View file

@ -1,11 +1,9 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Backend import Backend
from UM.Job import Job
from UM.Logger import Logger
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Vector import Vector
from UM.Message import Message
from cura.Scene.CuraSceneNode import CuraSceneNode
@ -13,7 +11,8 @@ from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from cura import LayerDataBuilder
from cura.CuraApplication import CuraApplication
from cura.LayerDataBuilder import LayerDataBuilder
from cura.LayerDataDecorator import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.Scene.GCodeListDecorator import GCodeListDecorator
@ -23,16 +22,16 @@ import numpy
import math
import re
from typing import Dict, List, NamedTuple, Optional, Union
from collections import namedtuple
Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", float)])
PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])])
Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])])
## This parser is intended to interpret the common firmware codes among all the
# different flavors
class FlavorParser:
def __init__(self) -> None:
Application.getInstance().hideMessageSignal.connect(self._onHideMessage)
CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
self._cancelled = False
self._message = None
self._layer_number = 0
@ -40,21 +39,21 @@ class FlavorParser:
self._clearValues()
self._scene_node = None
# X, Y, Z position, F feedrate and E extruder values are stored
self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e'])
self._position = Position
self._is_layers_in_file = False # Does the Gcode have the layers comment?
self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._extruder_offsets = {} # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._current_layer_thickness = 0.2 # default
self._filament_diameter = 2.85 # default
Application.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)
CuraApplication.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)
def _clearValues(self) -> None:
self._extruder_number = 0
self._extrusion_length_offset = [0]
self._extrusion_length_offset = [0] # type: List[float]
self._layer_type = LayerPolygon.Inset0Type
self._layer_number = 0
self._previous_z = 0
self._layer_data_builder = LayerDataBuilder.LayerDataBuilder()
self._previous_z = 0 # type: float
self._layer_data_builder = LayerDataBuilder()
self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91)
self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83)
@ -77,14 +76,14 @@ class FlavorParser:
def _getInt(self, line: str, code: str) -> Optional[int]:
value = self._getValue(line, code)
try:
return int(value)
return int(value) # type: ignore
except:
return None
def _getFloat(self, line: str, code: str) -> Optional[float]:
value = self._getValue(line, code)
try:
return float(value)
return float(value) # type: ignore
except:
return None
@ -92,10 +91,6 @@ class FlavorParser:
if message == self._message:
self._cancelled = True
@staticmethod
def _getNullBoundingBox() -> AxisAlignedBox:
return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10))
def _createPolygon(self, layer_thickness: float, path: List[List[Union[float, int]]], extruder_offsets: List[float]) -> bool:
countvalid = 0
for point in path:
@ -169,7 +164,7 @@ class FlavorParser:
return 0.35
return line_width
def _gCode0(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position:
def _gCode0(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
x, y, z, f, e = position
if self._is_absolute_positioning:
@ -205,7 +200,7 @@ class FlavorParser:
_gCode1 = _gCode0
## Home the head.
def _gCode28(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position:
def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
return self._position(
params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y,
@ -214,20 +209,20 @@ class FlavorParser:
position.e)
## Set the absolute positioning
def _gCode90(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position:
def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
self._is_absolute_positioning = True
self._is_absolute_extrusion = True
return position
## Set the relative positioning
def _gCode91(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position:
def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
self._is_absolute_positioning = False
self._is_absolute_extrusion = False
return position
## Reset the current position to the values specified.
# For example: G92 X10 will set the X to 10 without any physical motion.
def _gCode92(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position:
def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
if params.e is not None:
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
@ -260,7 +255,7 @@ class FlavorParser:
f = float(item[1:]) / 60
if item[0] == "E":
e = float(item[1:])
params = self._position(x, y, z, f, e)
params = PositionOptional(x, y, z, f, e)
return func(position, params, path)
return position
@ -290,13 +285,10 @@ class FlavorParser:
Logger.log("d", "Preparing to load GCode")
self._cancelled = False
# We obtain the filament diameter from the selected extruder to calculate line widths
global_stack = Application.getInstance().getGlobalContainerStack()
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
self._filament_diameter = global_stack.extruders[str(self._extruder_number)].getProperty("material_diameter", "value")
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
gcode_list = []
self._is_layers_in_file = False
@ -322,13 +314,14 @@ class FlavorParser:
lifetime=0,
title = catalog.i18nc("@info:title", "G-code Details"))
assert(self._message is not None) # use for typing purposes
self._message.setProgress(0)
self._message.show()
Logger.log("d", "Parsing Gcode...")
current_position = self._position(0, 0, 0, 0, [0])
current_path = []
current_position = Position(0, 0, 0, 0, [0])
current_path = [] #type: List[List[float]]
min_layer_number = 0
negative_layers = 0
previous_layer = 0
@ -443,9 +436,9 @@ class FlavorParser:
scene_node.addDecorator(gcode_list_decorator)
# gcode_dict stores gcode_lists for a number of build plates.
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_dict = {active_build_plate_id: gcode_list}
Application.getInstance().getController().getScene().gcode_dict = gcode_dict
CuraApplication.getInstance().getController().getScene().gcode_dict = gcode_dict #type: ignore #Because gcode_dict is generated dynamically.
Logger.log("d", "Finished parsing Gcode")
self._message.hide()
@ -453,7 +446,7 @@ class FlavorParser:
if self._layer_number == 0:
Logger.log("w", "File doesn't contain any valid layers")
settings = Application.getInstance().getGlobalContainerStack()
settings = CuraApplication.getInstance().getGlobalContainerStack()
if not settings.getProperty("machine_center_is_zero", "value"):
machine_width = settings.getProperty("machine_width", "value")
machine_depth = settings.getProperty("machine_depth", "value")
@ -461,7 +454,7 @@ class FlavorParser:
Logger.log("d", "GCode loading finished")
if Application.getInstance().getPreferences().getValue("gcodereader/show_caution"):
if CuraApplication.getInstance().getPreferences().getValue("gcodereader/show_caution"):
caution_message = Message(catalog.i18nc(
"@info:generic",
"Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."),
@ -470,7 +463,7 @@ class FlavorParser:
caution_message.show()
# The "save/print" button's state is bound to the backend state.
backend = Application.getInstance().getBackend()
backend = CuraApplication.getInstance().getBackend()
backend.backendStateChange.emit(Backend.BackendState.Disabled)
return scene_node

View file

@ -19,16 +19,16 @@ MimeTypeDatabase.addMimeType(
)
)
# Class for loading and parsing G-code files
class GCodeReader(MeshReader):
_flavor_default = "Marlin"
_flavor_keyword = ";FLAVOR:"
_flavor_readers_dict = {"RepRap" : RepRapFlavorParser.RepRapFlavorParser(),
"Marlin" : MarlinFlavorParser.MarlinFlavorParser()}
def __init__(self, application):
super(GCodeReader, self).__init__(application)
def __init__(self) -> None:
super().__init__()
self._supported_extensions = [".gcode", ".g"]
self._flavor_reader = None

View file

@ -20,7 +20,8 @@ def getMetaData():
]
}
def register(app):
app.addNonSliceableExtension(".gcode")
app.addNonSliceableExtension(".g")
return { "mesh_reader": GCodeReader.GCodeReader(app) }
return {"mesh_reader": GCodeReader.GCodeReader()}

View file

@ -140,7 +140,7 @@ class GCodeWriter(MeshWriter):
serialized = flat_global_container.serialize()
data = {"global_quality": serialized}
all_setting_keys = set(flat_global_container.getAllKeys())
all_setting_keys = flat_global_container.getAllKeys()
for extruder in sorted(stack.extruders.values(), key = lambda k: int(k.getMetaDataEntry("position"))):
extruder_quality = extruder.qualityChanges
if extruder_quality.getId() == "empty_quality_changes":
@ -167,7 +167,7 @@ class GCodeWriter(MeshWriter):
extruder_serialized = flat_extruder_quality.serialize()
data.setdefault("extruder_quality", []).append(extruder_serialized)
all_setting_keys.update(set(flat_extruder_quality.getAllKeys()))
all_setting_keys.update(flat_extruder_quality.getAllKeys())
# Check if there is any profiles
if not all_setting_keys:

View file

@ -17,8 +17,8 @@ from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
class ImageReader(MeshReader):
def __init__(self, application):
super(ImageReader, self).__init__(application)
def __init__(self) -> None:
super().__init__()
self._supported_extensions = [".jpg", ".jpeg", ".bmp", ".gif", ".png"]
self._ui = ImageReaderUI(self)

View file

@ -32,5 +32,6 @@ def getMetaData():
]
}
def register(app):
return { "mesh_reader": ImageReader.ImageReader(app) }
return {"mesh_reader": ImageReader.ImageReader()}

View file

@ -1,9 +1,14 @@
# This PostProcessing Plugin script is released
# under the terms of the AGPLv3 or higher
from typing import Optional, Tuple
from UM.Logger import Logger
from ..Script import Script
class FilamentChange(Script):
_layer_keyword = ";LAYER:"
def __init__(self):
super().__init__()
@ -64,11 +69,26 @@ class FilamentChange(Script):
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")
if layer_num <= len(data):
index, layer_data = self._searchLayerData(data, layer_num - 1)
if layer_data is None:
Logger.log("e", "Could not found the layer")
continue
lines = layer_data.split("\n")
lines.insert(2, color_change)
final_line = "\n".join(lines)
data[layer_num - 1] = final_line
data[index] = final_line
return data
## This method returns the data corresponding with the indicated layer number, looking in the gcode for
# the occurrence of this layer number.
def _searchLayerData(self, data: list, layer_num: int) -> Tuple[int, Optional[str]]:
for index, layer_data in enumerate(data):
first_line = layer_data.split("\n")[0]
# The first line should contain the layer number at the beginning.
if first_line[:len(self._layer_keyword)] == self._layer_keyword:
# If found the layer that we are looking for, then return the data
if first_line[len(self._layer_keyword):] == str(layer_num):
return index, layer_data
return 0, None

View file

@ -105,14 +105,6 @@ class PauseAtHeight(Script):
"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
}
}
}"""
@ -144,7 +136,6 @@ class PauseAtHeight(Script):
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")
@ -152,6 +143,8 @@ class PauseAtHeight(Script):
layer_0_z = 0.
current_z = 0
got_first_g_cmd_on_layer_0 = False
current_t = 0 #Tracks the current extruder for tracking the target temperature.
target_temperature = {} #Tracks the current target temperature for each extruder.
nbr_negative_layers = 0
@ -169,6 +162,16 @@ class PauseAtHeight(Script):
if not layers_started:
continue
#Track the latest printing temperature in order to resume at the correct temperature.
if line.startswith("T"):
current_t = self.getValue(line, "T")
m = self.getValue(line, "M")
if m is not None and (m == 104 or m == 109) and self.getValue(line, "S") is not None:
extruder = current_t
if self.getValue(line, "T") is not None:
extruder = self.getValue(line, "T")
target_temperature[extruder] = self.getValue(line, "S")
# If a Z instruction is in the line, read the current Z
if self.getValue(line, "Z") is not None:
current_z = self.getValue(line, "Z")
@ -262,9 +265,6 @@ class PauseAtHeight(Script):
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"
@ -272,7 +272,7 @@ class PauseAtHeight(Script):
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"
prepend_gcode += self.putValue(M = 109, S = int(target_temperature.get(current_t, default = 0))) + "; resume temperature\n"
# Push the filament back,
if retraction_amount != 0:

View file

@ -1,22 +1,17 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
import os.path
from PyQt5.QtCore import Qt, QTimer
from PyQt5.QtWidgets import QApplication
from UM.Math.Vector import Vector
from UM.Tool import Tool
from UM.Application import Application
from UM.Event import Event, MouseEvent
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.CuraApplication import CuraApplication
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.PickingPass import PickingPass
from UM.Operations.GroupedOperation import GroupedOperation
@ -26,8 +21,6 @@ from cura.Operations.SetParentOperation import SetParentOperation
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from UM.Scene.GroupDecorator import GroupDecorator
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
from UM.Settings.SettingInstance import SettingInstance
@ -38,7 +31,7 @@ class SupportEraser(Tool):
self._controller = self.getController()
self._selection_pass = None
Application.getInstance().globalContainerStackChanged.connect(self._updateEnabled)
CuraApplication.getInstance().globalContainerStackChanged.connect(self._updateEnabled)
# Note: if the selection is cleared with this tool active, there is no way to switch to
# another tool than to reselect an object (by clicking it) because the tool buttons in the
@ -106,7 +99,7 @@ class SupportEraser(Tool):
mesh.addCube(10,10,10)
node.setMeshData(mesh.build())
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
node.addDecorator(BuildPlateDecorator(active_build_plate))
node.addDecorator(SliceableObjectDecorator())
@ -126,7 +119,7 @@ class SupportEraser(Tool):
op.push()
node.setPosition(position, CuraSceneNode.TransformSpace.World)
Application.getInstance().getController().getScene().sceneChanged.emit(node)
CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)
def _removeEraserMesh(self, node: CuraSceneNode):
parent = node.getParent()
@ -139,16 +132,16 @@ class SupportEraser(Tool):
if parent and not Selection.isSelected(parent):
Selection.add(parent)
Application.getInstance().getController().getScene().sceneChanged.emit(node)
CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node)
def _updateEnabled(self):
plugin_enabled = False
global_container_stack = Application.getInstance().getGlobalContainerStack()
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
if global_container_stack:
plugin_enabled = global_container_stack.getProperty("anti_overhang_mesh", "enabled")
Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled)
CuraApplication.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled)
def _onSelectionChanged(self):
# When selection is passed from one object to another object, first the selection is cleared

View file

@ -9,4 +9,4 @@ def getMetaData():
def register(app):
return {"extension": Toolbox.Toolbox()}
return {"extension": Toolbox.Toolbox(app)}

View file

@ -92,6 +92,12 @@ Item
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text_medium")
}
Label
{
text: catalog.i18nc("@label", "Downloads") + ":"
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text_medium")
}
}
Column
{
@ -138,6 +144,12 @@ Item
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
}
Label
{
text: details.download_count || catalog.i18nc("@label", "Unknown")
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text")
}
}
Rectangle
{

View file

@ -12,7 +12,7 @@ Column
height: childrenRect.height
width: parent.width
spacing: UM.Theme.getSize("default_margin").height
/* Hidden for 3.4
Label
{
id: heading
@ -21,7 +21,6 @@ Column
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
}
*/
GridLayout
{
id: grid

View file

@ -32,6 +32,7 @@ class PackagesModel(ListModel):
self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed
self.addRoleName(Qt.UserRole + 16, "has_configs")
self.addRoleName(Qt.UserRole + 17, "supported_configs")
self.addRoleName(Qt.UserRole + 18, "download_count")
# List of filters for queries. The result is the union of the each list of results.
self._filter = {} # type: Dict[str, str]
@ -76,7 +77,9 @@ class PackagesModel(ListModel):
"is_enabled": package["is_enabled"] if "is_enabled" in package else False,
"is_installed": package["is_installed"] if "is_installed" in package else False,
"has_configs": has_configs,
"supported_configs": configs_model
"supported_configs": configs_model,
"download_count": package["download_count"] if "download_count" in package else 0
})
# Filter on all the key-word arguments.

View file

@ -1,19 +1,20 @@
# Copyright (c) 2018 Ultimaker B.V.
# Toolbox is released under the terms of the LGPLv3 or higher.
from typing import Dict, Optional, Union, Any
from typing import Dict, Optional, Union, Any, cast
import json
import os
import tempfile
import platform
from typing import List
from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from UM.Application import Application
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.Extension import Extension
from UM.Qt.ListModel import ListModel
from UM.i18n import i18nCatalog
from UM.Version import Version
@ -27,44 +28,39 @@ i18n_catalog = i18nCatalog("cura")
## The Toolbox class is responsible of communicating with the server through the API
class Toolbox(QObject, Extension):
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" #type: str
DEFAULT_CLOUD_API_VERSION = 1 #type: int
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com"
DEFAULT_CLOUD_API_VERSION = 1
def __init__(self, application: CuraApplication) -> None:
super().__init__()
def __init__(self, parent=None) -> None:
super().__init__(parent)
self._application = application #type: CuraApplication
self._application = Application.getInstance()
self._package_manager = None
self._plugin_registry = Application.getInstance().getPluginRegistry()
self._sdk_version = None
self._cloud_api_version = None
self._cloud_api_root = None
self._api_url = None
self._sdk_version = None # type: Optional[int]
self._cloud_api_version = None # type: Optional[int]
self._cloud_api_root = None # type: Optional[str]
self._api_url = None # type: Optional[str]
# Network:
self._get_packages_request = None
self._get_showcase_request = None
self._download_request = None
self._download_reply = None
self._download_progress = 0
self._is_downloading = False
self._network_manager = None
self._download_request = None #type: Optional[QNetworkRequest]
self._download_reply = None #type: Optional[QNetworkReply]
self._download_progress = 0 #type: float
self._is_downloading = False #type: bool
self._network_manager = None #type: Optional[QNetworkAccessManager]
self._request_header = [
b"User-Agent",
str.encode(
"%s/%s (%s %s)" % (
Application.getInstance().getApplicationName(),
Application.getInstance().getVersion(),
self._application.getApplicationName(),
self._application.getVersion(),
platform.system(),
platform.machine(),
)
)
]
self._request_urls = {}
self._to_update = [] # Package_ids that are waiting to be updated
self._old_plugin_ids = []
self._request_urls = {} # type: Dict[str, QUrl]
self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated
self._old_plugin_ids = [] # type: List[str]
# Data:
self._metadata = {
@ -76,7 +72,7 @@ class Toolbox(QObject, Extension):
"materials_showcase": [],
"materials_available": [],
"materials_installed": []
}
} # type: Dict[str, List[Any]]
# Models:
self._models = {
@ -88,33 +84,33 @@ class Toolbox(QObject, Extension):
"materials_showcase": AuthorsModel(self),
"materials_available": PackagesModel(self),
"materials_installed": PackagesModel(self)
}
} # type: Dict[str, ListModel]
# These properties are for keeping track of the UI state:
# ----------------------------------------------------------------------
# View category defines which filter to use, and therefore effectively
# which category is currently being displayed. For example, possible
# values include "plugin" or "material", but also "installed".
self._view_category = "plugin"
self._view_category = "plugin" #type: str
# View page defines which type of page layout to use. For example,
# possible values include "overview", "detail" or "author".
self._view_page = "loading"
self._view_page = "loading" #type: str
# Active package refers to which package is currently being downloaded,
# installed, or otherwise modified.
self._active_package = None
self._active_package = None # type: Optional[Dict[str, Any]]
self._dialog = None
self._restart_required = False
self._dialog = None #type: Optional[QObject]
self._restart_required = False #type: bool
# 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 = ""
self._license_dialog_plugin_name = "" #type: str
self._license_dialog_license_content = "" #type: str
self._license_dialog_plugin_file_location = "" #type: str
self._restart_dialog_message = "" #type: str
Application.getInstance().initializationFinished.connect(self._onAppInitialized)
self._application.initializationFinished.connect(self._onAppInitialized)
@ -156,7 +152,8 @@ class Toolbox(QObject, Extension):
# This is a plugin, so most of the components required are not ready when
# this is initialized. Therefore, we wait until the application is ready.
def _onAppInitialized(self) -> None:
self._package_manager = Application.getInstance().getPackageManager()
self._plugin_registry = self._application.getPluginRegistry()
self._package_manager = self._application.getPackageManager()
self._sdk_version = self._getSDKVersion()
self._cloud_api_version = self._getCloudAPIVersion()
self._cloud_api_root = self._getCloudAPIRoot()
@ -178,38 +175,38 @@ class Toolbox(QObject, Extension):
def _getCloudAPIRoot(self) -> str:
if not hasattr(cura, "CuraVersion"):
return self.DEFAULT_CLOUD_API_ROOT
if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"):
if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore
return self.DEFAULT_CLOUD_API_ROOT
if not cura.CuraVersion.CuraCloudAPIRoot:
if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore
return self.DEFAULT_CLOUD_API_ROOT
return cura.CuraVersion.CuraCloudAPIRoot
return cura.CuraVersion.CuraCloudAPIRoot # type: ignore
# Get the cloud API version from CuraVersion
def _getCloudAPIVersion(self) -> int:
if not hasattr(cura, "CuraVersion"):
return self.DEFAULT_CLOUD_API_VERSION
if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"):
if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore
return self.DEFAULT_CLOUD_API_VERSION
if not cura.CuraVersion.CuraCloudAPIVersion:
if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore
return self.DEFAULT_CLOUD_API_VERSION
return cura.CuraVersion.CuraCloudAPIVersion
return cura.CuraVersion.CuraCloudAPIVersion # type: ignore
# Get the packages version depending on Cura version settings.
def _getSDKVersion(self) -> int:
if not hasattr(cura, "CuraVersion"):
return self._plugin_registry.APIVersion
if not hasattr(cura.CuraVersion, "CuraSDKVersion"):
if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
return self._plugin_registry.APIVersion
if not cura.CuraVersion.CuraSDKVersion:
if not cura.CuraVersion.CuraSDKVersion: # type: ignore
return self._plugin_registry.APIVersion
return cura.CuraVersion.CuraSDKVersion
return cura.CuraVersion.CuraSDKVersion # type: ignore
@pyqtSlot()
def browsePackages(self) -> None:
# Create the network manager:
# This was formerly its own function but really had no reason to be as
# it was never called more than once ever.
if self._network_manager:
if self._network_manager is not None:
self._network_manager.finished.disconnect(self._onRequestFinished)
self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccessibleChanged)
self._network_manager = QNetworkAccessManager()
@ -235,11 +232,11 @@ class Toolbox(QObject, Extension):
def _createDialog(self, qml_name: str) -> Optional[QObject]:
Logger.log("d", "Toolbox: Creating dialog [%s].", qml_name)
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "resources", "qml", qml_name)
dialog = Application.getInstance().createQmlComponent(path, {"toolbox": self})
dialog = self._application.createQmlComponent(path, {"toolbox": self})
return dialog
def _convertPluginMetadata(self, plugin: dict) -> dict:
def _convertPluginMetadata(self, plugin: Dict[str, Any]) -> Dict[str, Any]:
formatted = {
"package_id": plugin["id"],
"package_type": "plugin",
@ -257,7 +254,6 @@ class Toolbox(QObject, Extension):
@pyqtSlot()
def _updateInstalledModels(self) -> None:
# This is moved here to avoid code duplication and so that after installing plugins they get removed from the
# list of old plugins
old_plugin_ids = self._plugin_registry.getInstalledPlugins()
@ -265,7 +261,7 @@ class Toolbox(QObject, Extension):
scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs()
self._old_plugin_ids = []
self._old_plugin_metadata = []
self._old_plugin_metadata = [] # type: List[Dict[str, Any]]
for plugin_id in old_plugin_ids:
# Neither the installed packages nor the packages that are scheduled to remove are old plugins
@ -353,8 +349,8 @@ class Toolbox(QObject, Extension):
return self._restart_required
@pyqtSlot()
def restart(self):
CuraApplication.getInstance().windowClosed()
def restart(self) -> None:
self._application.windowClosed()
def getRemotePackage(self, package_id: str) -> Optional[Dict]:
# TODO: make the lookup in a dict, not a loop. canUpdate is called for every item.
@ -432,7 +428,8 @@ class Toolbox(QObject, Extension):
Logger.log("i", "Toolbox: Requesting %s metadata from server.", type)
request = QNetworkRequest(self._request_urls[type])
request.setRawHeader(*self._request_header)
self._network_manager.get(request)
if self._network_manager:
self._network_manager.get(request)
@pyqtSlot(str)
def startDownload(self, url: str) -> None:
@ -441,15 +438,15 @@ class Toolbox(QObject, Extension):
self._download_request = QNetworkRequest(url)
if hasattr(QNetworkRequest, "FollowRedirectsAttribute"):
# Patch for Qt 5.6-5.8
self._download_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
if hasattr(QNetworkRequest, "RedirectPolicyAttribute"):
# Patch for Qt 5.9+
self._download_request.setAttribute(QNetworkRequest.RedirectPolicyAttribute, True)
self._download_request.setRawHeader(*self._request_header)
self._download_reply = self._network_manager.get(self._download_request)
cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.RedirectPolicyAttribute, True)
cast(QNetworkRequest, self._download_request).setRawHeader(*self._request_header)
self._download_reply = cast(QNetworkAccessManager, self._network_manager).get(self._download_request)
self.setDownloadProgress(0)
self.setIsDownloading(True)
self._download_reply.downloadProgress.connect(self._onDownloadProgress)
cast(QNetworkReply, self._download_reply).downloadProgress.connect(self._onDownloadProgress)
@pyqtSlot()
def cancelDownload(self) -> None:
@ -475,7 +472,6 @@ class Toolbox(QObject, Extension):
self.resetDownload()
def _onRequestFinished(self, reply: QNetworkReply) -> None:
if reply.error() == QNetworkReply.TimeoutError:
Logger.log("w", "Got a timeout.")
self.setViewPage("errored")
@ -551,12 +547,12 @@ class Toolbox(QObject, Extension):
self.setDownloadProgress(new_progress)
if bytes_sent == bytes_total:
self.setIsDownloading(False)
self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
cast(QNetworkReply, self._download_reply).downloadProgress.disconnect(self._onDownloadProgress)
# Must not delete the temporary file on Windows
self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
file_path = self._temp_plugin_file.name
# Write first and close, otherwise on Windows, it cannot read the file
self._temp_plugin_file.write(self._download_reply.readAll())
self._temp_plugin_file.write(cast(QNetworkReply, self._download_reply).readAll())
self._temp_plugin_file.close()
self._onDownloadComplete(file_path)
@ -577,13 +573,13 @@ class Toolbox(QObject, Extension):
# Getter & Setters for Properties:
# --------------------------------------------------------------------------
def setDownloadProgress(self, progress: Union[int, float]) -> None:
def setDownloadProgress(self, progress: float) -> None:
if progress != self._download_progress:
self._download_progress = progress
self.onDownloadProgressChanged.emit()
@pyqtProperty(int, fset = setDownloadProgress, notify = onDownloadProgressChanged)
def downloadProgress(self) -> int:
def downloadProgress(self) -> float:
return self._download_progress
def setIsDownloading(self, is_downloading: bool) -> None:

View file

@ -1,10 +1,12 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, cast, Set, Tuple, Union
from UM.FileHandler.FileHandler import FileHandler
from UM.FileHandler.FileWriter import FileWriter #To choose based on the output file mode (text vs. binary).
from UM.FileHandler.WriteFileJob import WriteFileJob #To call the file writer asynchronously.
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
@ -13,6 +15,7 @@ from UM.OutputDevice import OutputDeviceError #To show that something went wrong
from UM.Scene.SceneNode import SceneNode #For typing.
from UM.Version import Version #To check against firmware versions for support.
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
@ -45,15 +48,15 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# 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):
def __init__(self, device_id, address, properties, parent = None) -> 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._dummy_lambdas = set()
self._dummy_lambdas = ("", {}, io.BytesIO()) #type: Tuple[str, Dict, Union[io.StringIO, io.BytesIO]]
self._print_jobs = []
self._print_jobs = [] # type: List[PrintJobOutputModel]
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")
@ -61,18 +64,18 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# See comments about this hack with the clusterPrintersChanged signal
self.printersChanged.connect(self.clusterPrintersChanged)
self._accepts_commands = True
self._accepts_commands = True #type: bool
# Cluster does not have authentication, so default to authenticated
self._authentication_state = AuthState.Authenticated
self._error_message = None
self._write_job_progress_message = None
self._progress_message = None
self._error_message = None #type: Optional[Message]
self._write_job_progress_message = None #type: Optional[Message]
self._progress_message = None #type: Optional[Message]
self._active_printer = None # type: Optional[PrinterOutputModel]
self._printer_selection_dialog = None
self._printer_selection_dialog = None #type: QObject
self.setPriority(3) # Make sure the output device gets selected above local file output
self.setName(self._id)
@ -81,15 +84,15 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network"))
self._printer_uuid_to_unique_name_mapping = {}
self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str]
self._finished_jobs = []
self._finished_jobs = [] # type: List[PrintJobOutputModel]
self._cluster_size = int(properties.get(b"cluster_size", 0))
self._latest_reply_handler = None
self._latest_reply_handler = None #type: Optional[QNetworkReply]
def requestWrite(self, nodes: List[SceneNode], file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
self.writeStarted.emit(self)
self.sendMaterialProfiles()
@ -98,10 +101,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
if file_handler:
file_formats = file_handler.getSupportedFileTypesWrite()
else:
file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
file_formats = CuraApplication.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
#Create a list from the supported file formats string.
machine_file_formats = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("file_formats").split(";")
machine_file_formats = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("file_formats").split(";")
machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
#Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
if "application/x-ufp" not in machine_file_formats and self.printerType == "ultimaker3" and Version(self.firmwareVersion) >= Version("4.4"):
@ -118,9 +121,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
#Just take the first file format available.
if file_handler is not None:
writer = file_handler.getWriterByMimeType(preferred_format["mime_type"])
writer = file_handler.getWriterByMimeType(cast(str, preferred_format["mime_type"]))
else:
writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(preferred_format["mime_type"])
writer = CuraApplication.getInstance().getMeshFileHandler().getWriterByMimeType(cast(str, preferred_format["mime_type"]))
#This function pauses with the yield, waiting on instructions on which printer it needs to print with.
self._sending_job = self._sendPrintJob(writer, preferred_format, nodes)
@ -136,7 +139,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
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})
self._printer_selection_dialog = CuraApplication.getInstance().createQmlComponent(path, {"OutputDevice": self})
if self._printer_selection_dialog is not None:
self._printer_selection_dialog.show()
@ -182,10 +185,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
target_printer = yield #Potentially wait on the user to select a target printer.
# Using buffering greatly reduces the write time for many lines of gcode
stream = io.BytesIO() # type: Union[io.BytesIO, io.StringIO]# Binary mode.
if preferred_format["mode"] == FileWriter.OutputMode.TextMode:
stream = io.StringIO()
else: #Binary mode.
stream = io.BytesIO()
job = WriteFileJob(writer, stream, nodes, preferred_format["mode"])
@ -201,7 +204,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
yield True #Return that we had success!
yield #To prevent having to catch the StopIteration exception.
def _sendPrintJobWaitOnWriteJobFinished(self, job):
def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None:
self._write_job_progress_message.hide()
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
@ -222,40 +225,41 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# Add user name to the print_job
parts.append(self.createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))
file_name = Application.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"]
file_name = CuraApplication.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"]
output = stream.getvalue() #Either str or bytes depending on the output mode.
if isinstance(stream, io.StringIO):
output = output.encode("utf-8")
output = cast(str, output).encode("utf-8")
output = cast(bytes, output)
parts.append(self.createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output))
self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)
self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, on_finished = self._onPostPrintJobFinished, on_progress = self._onUploadPrintJobProgress)
@pyqtProperty(QObject, notify=activePrinterChanged)
@pyqtProperty(QObject, notify = activePrinterChanged)
def activePrinter(self) -> Optional[PrinterOutputModel]:
return self._active_printer
@pyqtSlot(QObject)
def setActivePrinter(self, printer: Optional[PrinterOutputModel]):
def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None:
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):
def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None:
self._progress_message.hide()
self._compressing_gcode = False
self._sending_gcode = False
def _onUploadPrintJobProgress(self, bytes_sent:int, bytes_total:int):
def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None:
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():
if self._progress_message and 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)
@ -272,16 +276,18 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._success_message.actionTriggered.connect(self._successMessageActionTriggered)
self._success_message.show()
else:
self._progress_message.setProgress(0)
self._progress_message.hide()
if self._progress_message is not None:
self._progress_message.setProgress(0)
self._progress_message.hide()
def _progressMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None:
def _progressMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None:
if action_id == "Abort":
Logger.log("d", "User aborted sending print to remote.")
self._progress_message.hide()
if self._progress_message is not None:
self._progress_message.hide()
self._compressing_gcode = False
self._sending_gcode = False
Application.getInstance().getController().setActiveStage("PrepareStage")
CuraApplication.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
@ -289,9 +295,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._latest_reply_handler.disconnect()
self._latest_reply_handler = None
def _successMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None:
def _successMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None:
if action_id == "View":
Application.getInstance().getController().setActiveStage("MonitorStage")
CuraApplication.getInstance().getController().setActiveStage("MonitorStage")
@pyqtSlot()
def openPrintJobControlPanel(self) -> None:
@ -303,21 +309,21 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
Logger.log("d", "Opening printer control panel...")
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
@pyqtProperty("QVariantList", notify=printJobsChanged)
def printJobs(self)-> List[PrintJobOutputModel] :
@pyqtProperty("QVariantList", notify = printJobsChanged)
def printJobs(self)-> List[PrintJobOutputModel]:
return self._print_jobs
@pyqtProperty("QVariantList", notify=printJobsChanged)
@pyqtProperty("QVariantList", notify = printJobsChanged)
def queuedPrintJobs(self) -> List[PrintJobOutputModel]:
return [print_job for print_job in self._print_jobs if print_job.state == "queued"]
@pyqtProperty("QVariantList", notify=printJobsChanged)
@pyqtProperty("QVariantList", notify = printJobsChanged)
def activePrintJobs(self) -> List[PrintJobOutputModel]:
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) -> List[PrinterOutputModel]:
printer_count = {}
@pyqtProperty("QVariantList", notify = clusterPrintersChanged)
def connectedPrintersTypeCount(self) -> List[Dict[str, str]]:
printer_count = {} # type: Dict[str, int]
for printer in self._printers:
if printer.type in printer_count:
printer_count[printer.type] += 1
@ -325,20 +331,20 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
printer_count[printer.type] = 1
result = []
for machine_type in printer_count:
result.append({"machine_type": machine_type, "count": printer_count[machine_type]})
result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])})
return result
@pyqtSlot(int, result=str)
@pyqtSlot(int, result = str)
def formatDuration(self, seconds: int) -> str:
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
@pyqtSlot(int, result=str)
@pyqtSlot(int, result = str)
def getTimeCompleted(self, time_remaining: int) -> str:
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)
@pyqtSlot(int, result = str)
def getDateCompleted(self, time_remaining: int) -> str:
current_time = time()
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
@ -373,10 +379,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self.sendMaterialProfiles()
def _update(self) -> None:
if not super()._update():
return
self.get("printers/", onFinished=self._onGetPrintersDataFinished)
self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)
super()._update()
self.get("printers/", on_finished = self._onGetPrintersDataFinished)
self.get("print_jobs/", on_finished = self._onGetPrintJobsFinished)
def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
if not checkValidGetReply(reply):
@ -415,7 +420,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
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)
job_list_changed = job_list_changed or self._removeJob(removed_job)
if job_list_changed:
self.printJobsChanged.emit() # Do a single emit for all print job changes.
@ -449,27 +454,27 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
if removed_printers or printer_list_changed:
self.printersChanged.emit()
def _createPrinterModel(self, data: Dict) -> PrinterOutputModel:
printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
number_of_extruders=self._number_of_extruders)
def _createPrinterModel(self, data: Dict[str, Any]) -> PrinterOutputModel:
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: Dict) -> PrintJobOutputModel:
def _createPrintJobModel(self, data: Dict[str, Any]) -> PrintJobOutputModel:
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: PrintJobOutputModel, data: Dict) -> None:
def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict[str, Any]) -> None:
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: PrinterOutputModel, data: Dict) -> None:
def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None:
# 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"]
@ -485,7 +490,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
printer.updateKey(data["uuid"])
printer.updateType(data["machine_variant"])
# Do not store the buildplate information that comes from connect if the current printer has not buildplate information
# Do not store the build plate information that comes from connect if the current printer has not build plate information
if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False):
printer.updateBuildplateName(data["build_plate"]["type"])
if not data["enabled"]:
@ -524,7 +529,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
brand=brand, color=color, name=name)
extruder.updateActiveMaterial(material)
def _removeJob(self, job: PrintJobOutputModel):
def _removeJob(self, job: PrintJobOutputModel) -> bool:
if job not in self._print_jobs:
return False
@ -535,7 +540,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
return True
def _removePrinter(self, printer: PrinterOutputModel):
def _removePrinter(self, printer: PrinterOutputModel) -> None:
self._printers.remove(printer)
if self._active_printer == printer:
self._active_printer = None
@ -549,16 +554,16 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
job = SendMaterialJob(device = self)
job.run()
def loadJsonFromReply(reply):
def loadJsonFromReply(reply: QNetworkReply) -> Optional[List[Dict[str, Any]]]:
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 None
return result
def checkValidGetReply(reply):
def checkValidGetReply(reply: QNetworkReply) -> bool:
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if status_code != 200:
@ -567,7 +572,8 @@ def checkValidGetReply(reply):
return True
def findByKey(list, key):
def findByKey(list: List[Union[PrintJobOutputModel, PrinterOutputModel]], key: str) -> Optional[PrintJobOutputModel]:
for item in list:
if item.key == key:
return item
return item
return None

View file

@ -1,43 +1,47 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
import time
from typing import Optional
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.Logger import Logger
from UM.i18n import i18nCatalog
from cura.CuraApplication import CuraApplication
from cura.MachineAction import MachineAction
from .UM3OutputDevicePlugin import UM3OutputDevicePlugin
catalog = i18nCatalog("cura")
class DiscoverUM3Action(MachineAction):
discoveredDevicesChanged = pyqtSignal()
def __init__(self):
def __init__(self) -> None:
super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network"))
self._qml_url = "DiscoverUM3Action.qml"
self._network_plugin = None
self._network_plugin = None #type: Optional[UM3OutputDevicePlugin]
self.__additional_components_context = None
self.__additional_component = None
self.__additional_components_view = None
self.__additional_components_view = None #type: Optional[QObject]
Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView)
CuraApplication.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView)
self._last_zero_conf_event_time = time.time()
self._last_zero_conf_event_time = time.time() #type: float
# Time to wait after a zero-conf service change before allowing a zeroconf reset
self._zero_conf_change_grace_period = 0.25
self._zero_conf_change_grace_period = 0.25 #type: float
@pyqtSlot()
def startDiscovery(self):
if not self._network_plugin:
Logger.log("d", "Starting device discovery.")
self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
self._network_plugin = CuraApplication.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
self._network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
self.discoveredDevicesChanged.emit()
@ -93,16 +97,16 @@ class DiscoverUM3Action(MachineAction):
return []
@pyqtSlot(str)
def setGroupName(self, group_name):
def setGroupName(self, group_name: str) -> None:
Logger.log("d", "Attempting to set the group name of the active machine to %s", group_name)
global_container_stack = Application.getInstance().getGlobalContainerStack()
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
if global_container_stack:
meta_data = global_container_stack.getMetaData()
if "connect_group_name" in meta_data:
previous_connect_group_name = meta_data["connect_group_name"]
global_container_stack.setMetaDataEntry("connect_group_name", group_name)
# Find all the places where there is the same group name and change it accordingly
Application.getInstance().getMachineManager().replaceContainersMetadata(key = "connect_group_name", value = previous_connect_group_name, new_value = group_name)
CuraApplication.getInstance().getMachineManager().replaceContainersMetadata(key = "connect_group_name", value = previous_connect_group_name, new_value = group_name)
else:
global_container_stack.addMetaDataEntry("connect_group_name", group_name)
global_container_stack.addMetaDataEntry("hidden", False)
@ -112,9 +116,9 @@ class DiscoverUM3Action(MachineAction):
self._network_plugin.reCheckConnections()
@pyqtSlot(str)
def setKey(self, key):
def setKey(self, key: str) -> None:
Logger.log("d", "Attempting to set the network key of the active machine to %s", key)
global_container_stack = Application.getInstance().getGlobalContainerStack()
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
if global_container_stack:
meta_data = global_container_stack.getMetaData()
if "um_network_key" in meta_data:
@ -124,7 +128,7 @@ class DiscoverUM3Action(MachineAction):
Logger.log("d", "Removing old authentication id %s for device %s", global_container_stack.getMetaDataEntry("network_authentication_id", None), key)
global_container_stack.removeMetaDataEntry("network_authentication_id")
global_container_stack.removeMetaDataEntry("network_authentication_key")
Application.getInstance().getMachineManager().replaceContainersMetadata(key = "um_network_key", value = previous_network_key, new_value = key)
CuraApplication.getInstance().getMachineManager().replaceContainersMetadata(key = "um_network_key", value = previous_network_key, new_value = key)
else:
global_container_stack.addMetaDataEntry("um_network_key", key)
@ -134,7 +138,7 @@ class DiscoverUM3Action(MachineAction):
@pyqtSlot(result = str)
def getStoredKey(self) -> str:
global_container_stack = Application.getInstance().getGlobalContainerStack()
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
if global_container_stack:
meta_data = global_container_stack.getMetaData()
if "um_network_key" in meta_data:
@ -149,12 +153,12 @@ class DiscoverUM3Action(MachineAction):
return ""
@pyqtSlot(str, result = bool)
def existsKey(self, key) -> bool:
return Application.getInstance().getMachineManager().existNetworkInstances(network_key = key)
def existsKey(self, key: str) -> bool:
return CuraApplication.getInstance().getMachineManager().existNetworkInstances(network_key = key)
@pyqtSlot()
def loadConfigurationFromPrinter(self):
machine_manager = Application.getInstance().getMachineManager()
def loadConfigurationFromPrinter(self) -> None:
machine_manager = CuraApplication.getInstance().getMachineManager()
hotend_ids = machine_manager.printerOutputDevices[0].hotendIds
for index in range(len(hotend_ids)):
machine_manager.printerOutputDevices[0].hotendIdChanged.emit(index, hotend_ids[index])
@ -162,16 +166,16 @@ class DiscoverUM3Action(MachineAction):
for index in range(len(material_ids)):
machine_manager.printerOutputDevices[0].materialIdChanged.emit(index, material_ids[index])
def _createAdditionalComponentsView(self):
def _createAdditionalComponentsView(self) -> None:
Logger.log("d", "Creating additional ui components for UM3.")
# Create networking dialog
path = os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml")
self.__additional_components_view = Application.getInstance().createQmlComponent(path, {"manager": self})
self.__additional_components_view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
if not self.__additional_components_view:
Logger.log("w", "Could not create ui components for UM3.")
return
# Create extra components
Application.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton"))
Application.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo"))
CuraApplication.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton"))
CuraApplication.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo"))

View file

@ -1,3 +1,8 @@
from typing import List, Optional
from UM.FileHandler.FileHandler import FileHandler
from UM.Scene.SceneNode import SceneNode
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
@ -9,12 +14,11 @@ 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.QtCore import QTimer
from PyQt5.QtWidgets import QMessageBox
from .LegacyUM3PrinterOutputController import LegacyUM3PrinterOutputController
@ -39,7 +43,7 @@ i18n_catalog = i18nCatalog("cura")
# 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):
def __init__(self, device_id, address: str, properties, parent = None) -> None:
super().__init__(device_id = device_id, address = address, properties = properties, parent = parent)
self._api_prefix = "/api/v1/"
self._number_of_extruders = 2
@ -125,7 +129,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
def connect(self):
super().connect()
self._setupMessages()
global_container = Application.getInstance().getGlobalContainerStack()
global_container = CuraApplication.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)
@ -168,7 +172,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
# NotImplementedError. We can simply ignore these.
pass
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
if not self.activePrinter:
# No active printer. Unable to write
return
@ -183,8 +187,8 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
self.writeStarted.emit(self)
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", [])
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_dict = getattr(CuraApplication.getInstance().getController().getScene(), "gcode_dict", [])
active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
@ -203,7 +207,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
for error in errors:
detailed_text += error + "\n"
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
text,
informative_text,
detailed_text,
@ -225,7 +229,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
for warning in warnings:
detailed_text += warning + "\n"
Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
text,
informative_text,
detailed_text,
@ -239,7 +243,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
self._startPrint()
# Notify the UI that a switch to the print monitor should happen
Application.getInstance().getController().setActiveStage("MonitorStage")
CuraApplication.getInstance().getController().setActiveStage("MonitorStage")
def _startPrint(self):
Logger.log("i", "Sending print job to printer.")
@ -264,7 +268,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
# Abort was called.
return
file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
file_name = "%s.gcode.gz" % CuraApplication.getInstance().getPrintInformation().jobName
self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode,
onFinished=self._onPostPrintJobFinished)
@ -276,7 +280,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
self._progress_message.hide()
self._compressing_gcode = False
self._sending_gcode = False
Application.getInstance().getController().setActiveStage("PrepareStage")
CuraApplication.getInstance().getController().setActiveStage("PrepareStage")
def _onPostPrintJobFinished(self, reply):
self._progress_message.hide()
@ -301,7 +305,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
if button == QMessageBox.Yes:
self._startPrint()
else:
Application.getInstance().getController().setActiveStage("PrepareStage")
CuraApplication.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.
@ -309,7 +313,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
def _checkForErrors(self):
errors = []
print_information = Application.getInstance().getPrintInformation()
print_information = CuraApplication.getInstance().getPrintInformation()
if not print_information.materialLengths:
Logger.log("w", "There is no material length information. Unable to check for errors.")
return errors
@ -329,7 +333,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
def _checkForWarnings(self):
warnings = []
print_information = Application.getInstance().getPrintInformation()
print_information = CuraApplication.getInstance().getPrintInformation()
if not print_information.materialLengths:
Logger.log("w", "There is no material length information. Unable to check for warnings.")
@ -452,7 +456,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
self._authentication_failed_message.show()
def _saveAuthentication(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
global_container_stack = CuraApplication.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)
@ -465,7 +469,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
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)
CuraApplication.getInstance().saveStack(global_container_stack)
Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id,
self._getSafeAuthKey())
else:
@ -496,7 +500,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
self._authentication_id = None
self.post("auth/request",
json.dumps({"application": "Cura-" + Application.getInstance().getVersion(),
json.dumps({"application": "Cura-" + CuraApplication.getInstance().getVersion(),
"user": self._getUserName()}).encode(),
onFinished=self._onRequestAuthenticationFinished)
@ -542,7 +546,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
"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"),
CuraApplication.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",

View file

@ -3,10 +3,10 @@
from UM.Logger import Logger
from UM.i18n import i18nCatalog
from UM.Application import Application
from UM.Qt.Duration import DurationFormat
from UM.PluginRegistry import PluginRegistry
from cura.CuraApplication import CuraApplication
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
@ -22,7 +22,7 @@ from threading import Thread, Event
from time import time, sleep
from queue import Queue
from enum import IntEnum
from typing import Union, Optional, List
from typing import Union, Optional, List, cast
import re
import functools # Used for reduce
@ -35,7 +35,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
firmwareProgressChanged = pyqtSignal()
firmwareUpdateStateChanged = pyqtSignal()
def __init__(self, serial_port: str, baud_rate: Optional[int] = None):
def __init__(self, serial_port: str, baud_rate: Optional[int] = None) -> None:
super().__init__(serial_port)
self.setName(catalog.i18nc("@item:inmenu", "USB printing"))
self.setShortDescription(catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print via USB"))
@ -68,7 +68,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._is_printing = False # A print is being sent.
## Set when print is started in order to check running time.
self._print_start_time = None # type: Optional[int]
self._print_start_time = None # type: Optional[float]
self._print_estimated_time = None # type: Optional[int]
self._accepts_commands = True
@ -83,7 +83,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self.setConnectionText(catalog.i18nc("@info:status", "Connected via USB"))
# Queue for commands that need to be sent.
self._command_queue = Queue()
self._command_queue = Queue() # type: Queue
# Event to indicate that an "ok" was received from the printer after sending a command.
self._command_received = Event()
self._command_received.set()
@ -107,11 +107,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# cancel any ongoing preheat timer before starting a print
self._printers[0].getController().stopPreheatTimers()
Application.getInstance().getController().setActiveStage("MonitorStage")
CuraApplication.getInstance().getController().setActiveStage("MonitorStage")
# find the G-code for the active build plate to print
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict")
active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_dict = getattr(CuraApplication.getInstance().getController().getScene(), "gcode_dict")
gcode_list = gcode_dict[active_build_plate_id]
self._printGCode(gcode_list)
@ -121,7 +121,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
def showFirmwareInterface(self):
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 = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
self._firmware_view.show()
@ -180,7 +180,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self.setFirmwareUpdateState(FirmwareUpdateState.completed)
# Try to re-connect with the machine again, which must be done on the Qt thread, so we use call later.
Application.getInstance().callLater(self.connect)
CuraApplication.getInstance().callLater(self.connect)
@pyqtProperty(float, notify = firmwareProgressChanged)
def firmwareProgress(self):
@ -214,7 +214,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._gcode_position = 0
self._print_start_time = time()
self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))
self._print_estimated_time = int(CuraApplication.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))
for i in range(0, 4): # Push first 4 entries before accepting other inputs
self._sendNextGcodeLine()
@ -250,7 +250,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
except SerialException:
Logger.log("w", "An exception occured while trying to create serial connection")
return
container_stack = Application.getInstance().getGlobalContainerStack()
container_stack = CuraApplication.getInstance().getGlobalContainerStack()
num_extruders = container_stack.getProperty("machine_extruder_count", "value")
# Ensure that a printer is created.
self._printers = [PrinterOutputModel(output_controller=GenericOutputController(self), number_of_extruders=num_extruders)]
@ -277,13 +277,12 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if self._serial is None or self._connection_state != ConnectionState.connected:
return
if type(command == str):
command = command.encode()
if not command.endswith(b"\n"):
command += b"\n"
new_command = cast(bytes, command) if type(command) is bytes else cast(str, command).encode() # type: bytes
if not new_command.endswith(b"\n"):
new_command += b"\n"
try:
self._command_received.clear()
self._serial.write(command)
self._serial.write(new_command)
except SerialTimeoutException:
Logger.log("w", "Timeout when sending command to printer via USB.")
self._command_received.set()

View file

@ -179,7 +179,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
return list(base_list)
__instance = None
__instance = None # type: USBPrinterOutputDeviceManager
@classmethod
def getInstance(cls, *args, **kwargs) -> "USBPrinterOutputDeviceManager":

View file

@ -44,20 +44,18 @@ class Profile:
# Parse the general section.
self._name = parser.get("general", "name")
self._type = parser.get("general", "type", fallback = None)
self._type = parser.get("general", "type")
self._weight = None
if "weight" in parser["general"]:
self._weight = int(parser.get("general", "weight"))
else:
self._weight = None
self._machine_type_id = parser.get("general", "machine_type", fallback = None)
self._machine_variant_name = parser.get("general", "machine_variant", fallback = None)
self._machine_instance_name = parser.get("general", "machine_instance", fallback = None)
self._machine_type_id = parser.get("general", "machine_type")
self._machine_variant_name = parser.get("general", "machine_variant")
self._machine_instance_name = parser.get("general", "machine_instance")
self._material_name = None
if "material" in parser["general"]: #Note: Material name is unused in this upgrade.
self._material_name = parser.get("general", "material")
elif self._type == "material":
self._material_name = parser.get("general", "name", fallback = None)
else:
self._material_name = None
self._material_name = parser.get("general", "name")
# Parse the settings.
self._settings = {} # type: Dict[str,str]

View file

@ -0,0 +1,91 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser
import io
from UM.VersionUpgrade import VersionUpgrade
## Upgrades configurations from the state they were in at version 3.4 to the
# state they should be in at version 4.0.
class VersionUpgrade34to40(VersionUpgrade):
## Gets the version number from a CFG file in Uranium's 3.3 format.
#
# Since the format may change, this is implemented for the 3.3 format only
# and needs to be included in the version upgrade system rather than
# globally in Uranium.
#
# \param serialised The serialised form of a CFG file.
# \return The version number stored in the CFG file.
# \raises ValueError The format of the version number in the file is
# incorrect.
# \raises KeyError The format of the file is incorrect.
def getCfgVersion(self, serialised):
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
setting_version = int(parser.get("metadata", "setting_version", fallback = 0))
return format_version * 1000000 + setting_version
## Upgrades Preferences to have the new version number.
def upgradePreferences(self, serialized, filename):
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
parser["general"]["version"] = "4"
if "metadata" not in parser:
parser["metadata"] = {}
parser["metadata"]["setting_version"] = "5"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
## Upgrades stacks to have the new version number.
def upgradeStack(self, serialized, filename):
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
parser["general"]["version"] = "4"
parser["metadata"]["setting_version"] = "5"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
## Upgrades instance containers to have the new version
# number.
def upgradeInstanceContainer(self, serialized, filename):
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
# Update version number.
parser["general"]["version"] = "4"
parser["metadata"]["setting_version"] = "5"
self._resetConcentric3DInfillPattern(parser)
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
def _resetConcentric3DInfillPattern(self, parser):
if "values" not in parser:
return
# Reset the patterns which are concentric 3d
for key in ("infill_pattern",
"support_pattern",
"support_interface_pattern",
"support_roof_pattern",
"support_bottom_pattern",
):
if key not in parser["values"]:
continue
if parser["values"][key] == "concentric_3d":
del parser["values"][key]

View file

@ -0,0 +1,52 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import VersionUpgrade34to40
upgrade = VersionUpgrade34to40.VersionUpgrade34to40()
def getMetaData():
return {
"version_upgrade": {
# From To Upgrade function
("preferences", 6000004): ("preferences", 6000005, upgrade.upgradePreferences),
("definition_changes", 4000004): ("definition_changes", 4000005, upgrade.upgradeInstanceContainer),
("quality_changes", 4000004): ("quality_changes", 4000005, upgrade.upgradeInstanceContainer),
("user", 4000004): ("user", 4000005, upgrade.upgradeInstanceContainer),
("machine_stack", 4000005): ("machine_stack", 4000005, upgrade.upgradeStack),
("extruder_train", 4000005): ("extruder_train", 4000005, upgrade.upgradeStack),
},
"sources": {
"preferences": {
"get_version": upgrade.getCfgVersion,
"location": {"."}
},
"machine_stack": {
"get_version": upgrade.getCfgVersion,
"location": {"./machine_instances"}
},
"extruder_train": {
"get_version": upgrade.getCfgVersion,
"location": {"./extruders"}
},
"definition_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./definition_changes"}
},
"quality_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality_changes"}
},
"user": {
"get_version": upgrade.getCfgVersion,
"location": {"./user"}
}
}
}
def register(app):
return { "version_upgrade": upgrade }

View file

@ -0,0 +1,8 @@
{
"name": "Version Upgrade 3.4 to 4.0",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 3.4 to Cura 4.0.",
"api": 4,
"i18n-catalog": "cura"
}

View file

@ -26,8 +26,8 @@ except ImportError:
DEFAULT_SUBDIV = 16 # Default subdivision factor for spheres, cones, and cylinders
EPSILON = 0.000001
class Shape:
class Shape:
# Expects verts in MeshBuilder-ready format, as a n by 3 mdarray
# with vertices stored in rows
def __init__(self, verts, faces, index_base, name):
@ -37,9 +37,10 @@ class Shape:
self.index_base = index_base
self.name = name
class X3DReader(MeshReader):
def __init__(self, application):
super().__init__(application)
def __init__(self) -> None:
super().__init__()
self._supported_extensions = [".x3d"]
self._namespaces = {}

View file

@ -15,5 +15,6 @@ def getMetaData():
]
}
def register(app):
return { "mesh_reader": X3DReader.X3DReader(app) }
return {"mesh_reader": X3DReader.X3DReader()}

View file

@ -6,8 +6,10 @@ import io
import json #To parse the product-to-id mapping file.
import os.path #To find the product-to-id mapping.
import sys
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, cast
import xml.etree.ElementTree as ET
from typing import Dict
from typing import Iterator
from UM.Resources import Resources
from UM.Logger import Logger
@ -132,7 +134,7 @@ class XmlMaterialProfile(InstanceContainer):
"version": self.CurrentFdmMaterialVersion})
## Begin Metadata Block
builder.start("metadata")
builder.start("metadata") # type: ignore
metadata = copy.deepcopy(self.getMetaData())
# setting_version is derived from the "version" tag in the schema, so don't serialize it into a file
@ -156,21 +158,21 @@ class XmlMaterialProfile(InstanceContainer):
metadata.pop("name", "")
## Begin Name Block
builder.start("name")
builder.start("name") # type: ignore
builder.start("brand")
builder.start("brand") # type: ignore
builder.data(metadata.pop("brand", ""))
builder.end("brand")
builder.start("material")
builder.start("material") # type: ignore
builder.data(metadata.pop("material", ""))
builder.end("material")
builder.start("color")
builder.start("color") # type: ignore
builder.data(metadata.pop("color_name", ""))
builder.end("color")
builder.start("label")
builder.start("label") # type: ignore
builder.data(self.getName())
builder.end("label")
@ -178,7 +180,7 @@ class XmlMaterialProfile(InstanceContainer):
## End Name Block
for key, value in metadata.items():
builder.start(key)
builder.start(key) # type: ignore
if value is not None: #Nones get handled well by the builder.
#Otherwise the builder always expects a string.
#Deserialize expects the stringified version.
@ -190,10 +192,10 @@ class XmlMaterialProfile(InstanceContainer):
## End Metadata Block
## Begin Properties Block
builder.start("properties")
builder.start("properties") # type: ignore
for key, value in properties.items():
builder.start(key)
builder.start(key) # type: ignore
builder.data(value)
builder.end(key)
@ -201,14 +203,14 @@ class XmlMaterialProfile(InstanceContainer):
## End Properties Block
## Begin Settings Block
builder.start("settings")
builder.start("settings") # type: ignore
if self.getMetaDataEntry("definition") == "fdmprinter":
for instance in self.findInstances():
self._addSettingElement(builder, instance)
machine_container_map = {}
machine_variant_map = {}
machine_container_map = {} # type: Dict[str, InstanceContainer]
machine_variant_map = {} # type: Dict[str, Dict[str, Any]]
variant_manager = CuraApplication.getInstance().getVariantManager()
@ -248,7 +250,7 @@ class XmlMaterialProfile(InstanceContainer):
product = product_name
break
builder.start("machine")
builder.start("machine") # type: ignore
builder.start("machine_identifier", {
"manufacturer": container.getMetaDataEntry("machine_manufacturer",
definition_metadata.get("manufacturer", "Unknown")),
@ -264,7 +266,7 @@ class XmlMaterialProfile(InstanceContainer):
self._addSettingElement(builder, instance)
# Find all hotend sub-profiles corresponding to this material and machine and add them to this profile.
buildplate_dict = {}
buildplate_dict = {} # type: Dict[str, Any]
for variant_name, variant_dict in machine_variant_map[definition_id].items():
variant_type = variant_dict["variant_node"].metadata["hardware_type"]
from cura.Machines.VariantManager import VariantType
@ -839,11 +841,14 @@ class XmlMaterialProfile(InstanceContainer):
if label is not None and label.text is not None:
base_metadata["name"] = label.text
else:
base_metadata["name"] = cls._profile_name(material.text, color.text)
if material is not None and color is not None:
base_metadata["name"] = cls._profile_name(material.text, color.text)
else:
base_metadata["name"] = "Unknown Material"
base_metadata["brand"] = brand.text if brand.text is not None else "Unknown Brand"
base_metadata["material"] = material.text if material.text is not None else "Unknown Type"
base_metadata["color_name"] = color.text if color.text is not None else "Unknown Color"
base_metadata["brand"] = brand.text if brand is not None and brand.text is not None else "Unknown Brand"
base_metadata["material"] = material.text if material is not None and material.text is not None else "Unknown Type"
base_metadata["color_name"] = color.text if color is not None and color.text is not None else "Unknown Color"
continue
#Setting_version is derived from the "version" tag in the schema earlier, so don't set it here.
@ -863,13 +868,13 @@ class XmlMaterialProfile(InstanceContainer):
tag_name = _tag_without_namespace(entry)
property_values[tag_name] = entry.text
base_metadata["approximate_diameter"] = str(round(float(property_values.get("diameter", 2.85)))) # In mm
base_metadata["approximate_diameter"] = str(round(float(cast(float, property_values.get("diameter", 2.85))))) # In mm
base_metadata["properties"] = property_values
base_metadata["definition"] = "fdmprinter"
compatible_entries = data.iterfind("./um:settings/um:setting[@key='hardware compatible']", cls.__namespaces)
try:
common_compatibility = cls._parseCompatibleValue(next(compatible_entries).text)
common_compatibility = cls._parseCompatibleValue(next(compatible_entries).text) # type: ignore
except StopIteration: #No 'hardware compatible' setting.
common_compatibility = True
base_metadata["compatible"] = common_compatibility
@ -883,7 +888,8 @@ class XmlMaterialProfile(InstanceContainer):
for entry in machine.iterfind("./um:setting", cls.__namespaces):
key = entry.get("key")
if key == "hardware compatible":
machine_compatibility = cls._parseCompatibleValue(entry.text)
if entry.text is not None:
machine_compatibility = cls._parseCompatibleValue(entry.text)
for identifier in machine.iterfind("./um:machine_identifier", cls.__namespaces):
machine_id_list = product_id_map.get(identifier.get("product"), [])
@ -891,11 +897,11 @@ class XmlMaterialProfile(InstanceContainer):
machine_id_list = cls.getPossibleDefinitionIDsFromName(identifier.get("product"))
for machine_id in machine_id_list:
definition_metadata = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id)
if not definition_metadata:
definition_metadatas = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id)
if not definition_metadatas:
continue
definition_metadata = definition_metadata[0]
definition_metadata = definition_metadatas[0]
machine_manufacturer = identifier.get("manufacturer", definition_metadata.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
@ -918,7 +924,7 @@ class XmlMaterialProfile(InstanceContainer):
result_metadata.append(new_material_metadata)
buildplates = machine.iterfind("./um:buildplate", cls.__namespaces)
buildplate_map = {}
buildplate_map = {} # type: Dict[str, Dict[str, bool]]
buildplate_map["buildplate_compatible"] = {}
buildplate_map["buildplate_recommended"] = {}
for buildplate in buildplates:
@ -939,10 +945,11 @@ class XmlMaterialProfile(InstanceContainer):
buildplate_recommended = True
for entry in settings:
key = entry.get("key")
if key == "hardware compatible":
buildplate_compatibility = cls._parseCompatibleValue(entry.text)
elif key == "hardware recommended":
buildplate_recommended = cls._parseCompatibleValue(entry.text)
if entry.text is not None:
if key == "hardware compatible":
buildplate_compatibility = cls._parseCompatibleValue(entry.text)
elif key == "hardware recommended":
buildplate_recommended = cls._parseCompatibleValue(entry.text)
buildplate_map["buildplate_compatible"][buildplate_id] = buildplate_compatibility
buildplate_map["buildplate_recommended"][buildplate_id] = buildplate_recommended
@ -956,7 +963,8 @@ class XmlMaterialProfile(InstanceContainer):
for entry in hotend.iterfind("./um:setting", cls.__namespaces):
key = entry.get("key")
if key == "hardware compatible":
hotend_compatibility = cls._parseCompatibleValue(entry.text)
if entry.text is not None:
hotend_compatibility = cls._parseCompatibleValue(entry.text)
new_hotend_specific_material_id = container_id + "_" + machine_id + "_" + hotend_name.replace(" ", "_")