Merge branch 'master' into feature_persistent_postprocessing

This commit is contained in:
Ghostkeeper 2018-03-22 19:19:05 +01:00
commit 9d63258703
No known key found for this signature in database
GPG key ID: 5252B696FB5E7C7A
797 changed files with 18318 additions and 12276 deletions

View file

@ -4,26 +4,27 @@
import os.path
import zipfile
import numpy
import Savitar
from UM.Application import Application
from UM.Logger import Logger
from UM.Math.Matrix import Matrix
from UM.Math.Vector import Vector
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Mesh.MeshReader import MeshReader
from UM.Scene.GroupDecorator import GroupDecorator
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
from UM.Application import Application
from cura.Settings.ExtruderManager import ExtruderManager
from cura.QualityManager import QualityManager
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
MYPY = False
import Savitar
import numpy
try:
if not MYPY:
import xml.etree.cElementTree as ET
@ -77,9 +78,9 @@ class ThreeMFReader(MeshReader):
self._object_count += 1
node_name = "Object %s" % self._object_count
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
um_node = CuraSceneNode()
um_node = CuraSceneNode() # This adds a SettingOverrideDecorator
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
um_node.setName(node_name)
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
@ -108,8 +109,6 @@ class ThreeMFReader(MeshReader):
# Add the setting override decorator, so we can add settings to this node.
if settings:
um_node.addDecorator(SettingOverrideDecorator())
global_container_stack = Application.getInstance().getGlobalContainerStack()
# Ensure the correct next container for the SettingOverride decorator is set.
@ -120,8 +119,8 @@ class ThreeMFReader(MeshReader):
um_node.callDecoration("setActiveExtruder", default_stack.getId())
# Get the definition & set it
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
um_node.callDecoration("getStack").getTop().setDefinition(definition.getId())
definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack.definition)
um_node.callDecoration("getStack").getTop().setDefinition(definition_id)
setting_container = um_node.callDecoration("getStack").getTop()
@ -138,7 +137,7 @@ class ThreeMFReader(MeshReader):
continue
setting_container.setProperty(key, "value", setting_value)
if len(um_node.getChildren()) > 0:
if len(um_node.getChildren()) > 0 and um_node.getMeshData() is None:
group_decorator = GroupDecorator()
um_node.addDecorator(group_decorator)
um_node.setSelectable(True)

File diff suppressed because it is too large Load diff

View file

@ -49,10 +49,10 @@ class WorkspaceDialog(QObject):
self._material_labels = []
self._extruders = []
self._objects_on_plate = False
self._is_printer_group = False
machineConflictChanged = pyqtSignal()
qualityChangesConflictChanged = pyqtSignal()
definitionChangesConflictChanged = pyqtSignal()
materialConflictChanged = pyqtSignal()
numVisibleSettingsChanged = pyqtSignal()
activeModeChanged = pyqtSignal()
@ -67,6 +67,16 @@ class WorkspaceDialog(QObject):
machineTypeChanged = pyqtSignal()
variantTypeChanged = pyqtSignal()
extrudersChanged = pyqtSignal()
isPrinterGroupChanged = pyqtSignal()
@pyqtProperty(bool, notify = isPrinterGroupChanged)
def isPrinterGroup(self) -> bool:
return self._is_printer_group
def setIsPrinterGroup(self, value: bool):
if value != self._is_printer_group:
self._is_printer_group = value
self.isPrinterGroupChanged.emit()
@pyqtProperty(str, notify=variantTypeChanged)
def variantType(self):
@ -196,10 +206,6 @@ class WorkspaceDialog(QObject):
def qualityChangesConflict(self):
return self._has_quality_changes_conflict
@pyqtProperty(bool, notify=definitionChangesConflictChanged)
def definitionChangesConflict(self):
return self._has_definition_changes_conflict
@pyqtProperty(bool, notify=materialConflictChanged)
def materialConflict(self):
return self._has_material_conflict
@ -229,18 +235,11 @@ class WorkspaceDialog(QObject):
self._has_quality_changes_conflict = quality_changes_conflict
self.qualityChangesConflictChanged.emit()
def setDefinitionChangesConflict(self, definition_changes_conflict):
if self._has_definition_changes_conflict != definition_changes_conflict:
self._has_definition_changes_conflict = definition_changes_conflict
self.definitionChangesConflictChanged.emit()
def getResult(self):
if "machine" in self._result and not self._has_machine_conflict:
self._result["machine"] = None
if "quality_changes" in self._result and not self._has_quality_changes_conflict:
self._result["quality_changes"] = None
if "definition_changes" in self._result and not self._has_definition_changes_conflict:
self._result["definition_changes"] = None
if "material" in self._result and not self._has_material_conflict:
self._result["material"] = None

View file

@ -108,7 +108,22 @@ UM.Dialog
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
ComboBox
{
model: resolveStrategiesModel
model: ListModel
{
Component.onCompleted:
{
append({"key": "override", "label": catalog.i18nc("@action:ComboBox option", "Update") + " " + manager.machineName});
append({"key": "new", "label": catalog.i18nc("@action:ComboBox option", "Create new")});
}
}
Connections
{
target: manager
onMachineNameChanged:
{
machineResolveComboBox.model.get(0).label = catalog.i18nc("@action:ComboBox option", "Update") + " " + manager.machineName;
}
}
textRole: "label"
id: machineResolveComboBox
width: parent.width
@ -141,7 +156,7 @@ UM.Dialog
height: childrenRect.height
Label
{
text: catalog.i18nc("@action:label", "Name")
text: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name")
width: (parent.width / 3) | 0
}
Label

View file

@ -1,14 +1,15 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
import configparser
from io import StringIO
import zipfile
from UM.Application import Application
from UM.Logger import Logger
from UM.Preferences import Preferences
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.ExtruderManager import ExtruderManager
import zipfile
from io import StringIO
import configparser
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
class ThreeMFWorkspaceWriter(WorkspaceWriter):
@ -16,7 +17,10 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
super().__init__()
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
mesh_writer = Application.getInstance().getMeshFileHandler().getWriter("3MFWriter")
application = Application.getInstance()
machine_manager = application.getMachineManager()
mesh_writer = application.getMeshFileHandler().getWriter("3MFWriter")
if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace
return False
@ -29,17 +33,17 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
if archive is None: # This happens if there was no mesh data to write.
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
global_container_stack = Application.getInstance().getGlobalContainerStack()
global_stack = machine_manager.activeMachine
# Add global container stack data to the archive.
self._writeContainerToArchive(global_container_stack, archive)
self._writeContainerToArchive(global_stack, archive)
# Also write all containers in the stack to the file
for container in global_container_stack.getContainers():
for container in global_stack.getContainers():
self._writeContainerToArchive(container, archive)
# Check if the machine has extruders and save all that data as well.
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_container_stack.getId()):
for extruder_stack in global_stack.extruders.values():
self._writeContainerToArchive(extruder_stack, archive)
for container in extruder_stack.getContainers():
self._writeContainerToArchive(container, archive)
@ -57,11 +61,11 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
# Save Cura version
version_file = zipfile.ZipInfo("Cura/version.ini")
version_config_parser = configparser.ConfigParser()
version_config_parser = configparser.ConfigParser(interpolation = None)
version_config_parser.add_section("versions")
version_config_parser.set("versions", "cura_version", Application.getInstance().getVersion())
version_config_parser.set("versions", "build_type", Application.getInstance().getBuildType())
version_config_parser.set("versions", "is_debug_mode", str(Application.getInstance().getIsDebugMode()))
version_config_parser.set("versions", "cura_version", application.getVersion())
version_config_parser.set("versions", "build_type", application.getBuildType())
version_config_parser.set("versions", "is_debug_mode", str(application.getIsDebugMode()))
version_file_string = StringIO()
version_config_parser.write(version_file_string)
@ -85,7 +89,8 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
# Some containers have a base file, which should then be the file to use.
if "base_file" in container.getMetaData():
base_file = container.getMetaDataEntry("base_file")
container = ContainerRegistry.getInstance().findContainers(id = base_file)[0]
if base_file != container.getId():
container = ContainerRegistry.getInstance().findContainers(id = base_file)[0]
file_name = "Cura/%s.%s" % (container.getId(), file_suffix)

View file

@ -68,7 +68,7 @@ class ThreeMFWriter(MeshWriter):
if not isinstance(um_node, SceneNode):
return None
active_build_plate_nr = CuraApplication.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate_nr = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
if um_node.callDecoration("getBuildPlateNumber") != active_build_plate_nr:
return

View file

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

View file

@ -1,3 +1,9 @@
[3.2.1]
*Bug fixes
- Fixed issues where Cura crashes on startup and loading profiles
- Updated translations
- Fixed an issue where the text would not render properly
[3.2.0]
*Tree support
Experimental tree-like support structure that uses branches to support prints. Branches grow and multiply towards the model, with fewer contact points than alternative support methods. This results in better surface finishes for organic-shaped prints.

View file

@ -10,7 +10,6 @@ from UM.Logger import Logger
from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Settings.Validator import ValidatorState #To find if a setting is in an error state. We can't slice then.
from UM.Platform import Platform
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Qt.Duration import DurationFormat
@ -32,7 +31,11 @@ import Arcus
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class CuraEngineBackend(QObject, Backend):
backendError = Signal()
## Starts the back-end plug-in.
#
# This registers all the signal listeners and prepares for communication
@ -59,23 +62,26 @@ class CuraEngineBackend(QObject, Backend):
default_engine_location = execpath
break
self._application = Application.getInstance()
self._multi_build_plate_model = None
self._machine_error_checker = None
if not default_engine_location:
raise EnvironmentError("Could not find CuraEngine")
Logger.log("i", "Found CuraEngine at: %s" %(default_engine_location))
Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
default_engine_location = os.path.abspath(default_engine_location)
Preferences.getInstance().addPreference("backend/location", default_engine_location)
# Workaround to disable layer view processing if layer view is not active.
self._layer_view_active = False
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
Application.getInstance().getBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveViewChanged)
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._scene = Application.getInstance().getController().getScene()
self._scene = self._application.getController().getScene()
self._scene.sceneChanged.connect(self._onSceneChanged)
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
@ -83,19 +89,10 @@ class CuraEngineBackend(QObject, Backend):
# - whenever there is a value change, we start the timer
# - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the
# auto-slicing timer when that error check is finished
# If there is an error check, it will set the "_is_error_check_scheduled" flag, stop the auto-slicing timer,
# and only wait for the error check to be finished to start the auto-slicing timer again.
# 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
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
Application.getInstance().getExtruderManager().extrudersAdded.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
# A flag indicating if an error check was scheduled
# If so, we will stop the auto-slice timer and start upon the error check
self._is_error_check_scheduled = False
# Listeners for receiving messages from the back-end.
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
@ -121,14 +118,8 @@ class CuraEngineBackend(QObject, Backend):
self._last_num_objects = defaultdict(int) # Count number of objects to see if there is something changed
self._postponed_scene_change_sources = [] # scene change is postponed (by a tool)
self.backendQuit.connect(self._onBackendQuit)
self.backendConnected.connect(self._onBackendConnected)
# When a tool operation is in progress, don't slice. So we need to listen for tool operations.
Application.getInstance().getController().toolOperationStarted.connect(self._onToolOperationStarted)
Application.getInstance().getController().toolOperationStopped.connect(self._onToolOperationStopped)
self._slice_start_time = None
self._is_disabled = False
Preferences.getInstance().addPreference("general/auto_slice", True)
@ -142,6 +133,30 @@ class CuraEngineBackend(QObject, Backend):
self.determineAutoSlicing()
Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
self._application.initializationFinished.connect(self.initialize)
def initialize(self):
self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
self._application.getController().activeViewChanged.connect(self._onActiveViewChanged)
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged)
self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
# extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash
ExtruderManager.getInstance().extrudersChanged.connect(self._extruderChanged)
self.backendQuit.connect(self._onBackendQuit)
self.backendConnected.connect(self._onBackendConnected)
# When a tool operation is in progress, don't slice. So we need to listen for tool operations.
self._application.getController().toolOperationStarted.connect(self._onToolOperationStarted)
self._application.getController().toolOperationStopped.connect(self._onToolOperationStopped)
self._machine_error_checker = self._application.getMachineErrorChecker()
self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished)
## Terminate the engine process.
#
# This function should terminate the engine process.
@ -187,14 +202,12 @@ class CuraEngineBackend(QObject, Backend):
## Manually triggers a reslice
@pyqtSlot()
def forceSlice(self):
if self._use_timer:
self._change_timer.start()
else:
self.slice()
self.markSliceAll()
self.slice()
## Perform a slice of the scene.
def slice(self):
Logger.log("d", "starting to slice!")
Logger.log("d", "Starting to slice...")
self._slice_start_time = time()
if not self._build_plates_to_be_sliced:
self.processingProgress.emit(1.0)
@ -202,17 +215,21 @@ class CuraEngineBackend(QObject, Backend):
return
if self._process_layers_job:
Logger.log("d", " ## Process layers job still busy, trying later")
Logger.log("d", "Process layers job still busy, trying later.")
return
if not hasattr(self._scene, "gcode_dict"):
self._scene.gcode_dict = {}
# see if we really have to slice
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
num_objects = self._numObjects()
num_objects = self._numObjectsPerBuildPlate()
self._stored_layer_data = []
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] = []
Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced)
@ -220,9 +237,6 @@ class CuraEngineBackend(QObject, Backend):
self.slice()
return
self._stored_layer_data = []
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
if Application.getInstance().getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
Application.getInstance().getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
@ -292,6 +306,7 @@ class CuraEngineBackend(QObject, Backend):
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
return
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
@ -300,6 +315,7 @@ class CuraEngineBackend(QObject, Backend):
"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()
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
else:
self.backendStateChange.emit(BackendState.NotStarted)
return
@ -328,6 +344,7 @@ class CuraEngineBackend(QObject, Backend):
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
else:
self.backendStateChange.emit(BackendState.NotStarted)
return
@ -350,6 +367,7 @@ class CuraEngineBackend(QObject, Backend):
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:
@ -358,6 +376,7 @@ class CuraEngineBackend(QObject, Backend):
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
else:
self.backendStateChange.emit(BackendState.NotStarted)
@ -367,9 +386,9 @@ class CuraEngineBackend(QObject, Backend):
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
else:
self.backendStateChange.emit(BackendState.NotStarted)
pass
self._invokeSlice()
return
@ -387,6 +406,7 @@ class CuraEngineBackend(QObject, Backend):
# - decorator isBlockSlicing is found (used in g-code reader)
def determineAutoSlicing(self):
enable_timer = True
self._is_disabled = False
if not Preferences.getInstance().getValue("general/auto_slice"):
enable_timer = False
@ -394,6 +414,7 @@ class CuraEngineBackend(QObject, Backend):
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
@ -409,7 +430,7 @@ class CuraEngineBackend(QObject, Backend):
return False
## Return a dict with number of objects per build plate
def _numObjects(self):
def _numObjectsPerBuildPlate(self):
num_objects = defaultdict(int)
for node in DepthFirstIterator(self._scene.getRoot()):
# Only count sliceable objects
@ -436,7 +457,7 @@ class CuraEngineBackend(QObject, Backend):
source_build_plate_number = source.callDecoration("getBuildPlateNumber")
if source == self._scene.getRoot():
# we got the root node
num_objects = self._numObjects()
num_objects = self._numObjectsPerBuildPlate()
for build_plate_number in list(self._last_num_objects.keys()) + list(num_objects.keys()):
if build_plate_number not in self._last_num_objects or num_objects[build_plate_number] != self._last_num_objects[build_plate_number]:
self._last_num_objects[build_plate_number] = num_objects[build_plate_number]
@ -500,7 +521,7 @@ class CuraEngineBackend(QObject, Backend):
node.getParent().removeChild(node)
def markSliceAll(self):
for build_plate_number in range(Application.getInstance().getBuildPlateModel().maxBuildPlate + 1):
for build_plate_number in range(Application.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1):
if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number)
@ -524,11 +545,13 @@ class CuraEngineBackend(QObject, Backend):
elif property == "validationState":
if self._use_timer:
self._is_error_check_scheduled = True
self._change_timer.stop()
def _onStackErrorCheckFinished(self):
self._is_error_check_scheduled = False
self.determineAutoSlicing()
if self._is_disabled:
return
if not self._slicing and self._build_plates_to_be_sliced:
self.needsSlicing()
self._onChanged()
@ -543,6 +566,8 @@ class CuraEngineBackend(QObject, Backend):
#
# \param message The protobuf message containing sliced layer data.
def _onOptimizedLayerMessage(self, message):
if self._start_slice_job_build_plate not in self._stored_optimized_layer_data:
self._stored_optimized_layer_data[self._start_slice_job_build_plate] = []
self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message)
## Called when a progress message is received from the engine.
@ -552,12 +577,15 @@ class CuraEngineBackend(QObject, Backend):
self.processingProgress.emit(message.amount)
self.backendStateChange.emit(BackendState.Processing)
# testing
def _invokeSlice(self):
if self._use_timer:
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
# otherwise business as usual
if self._is_error_check_scheduled:
if self._machine_error_checker is None:
self._change_timer.stop()
return
if self._machine_error_checker.needToWaitForResult:
self._change_timer.stop()
else:
self._change_timer.start()
@ -583,8 +611,13 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
# See if we need to process the sliced layers job.
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate:
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
if (
self._layer_view_active and
(self._process_layers_job is None or not self._process_layers_job.isRunning()) and
active_build_plate == self._start_slice_job_build_plate and
active_build_plate not in self._build_plates_to_be_sliced):
self._startProcessSlicedLayersJob(active_build_plate)
# self._onActiveViewChanged()
self._start_slice_job_build_plate = None
@ -623,7 +656,11 @@ class CuraEngineBackend(QObject, Backend):
if self._use_timer:
# if the error check is scheduled, wait for the error check finish signal to trigger auto-slice,
# otherwise business as usual
if self._is_error_check_scheduled:
if self._machine_error_checker is None:
self._change_timer.stop()
return
if self._machine_error_checker.needToWaitForResult:
self._change_timer.stop()
else:
self._change_timer.start()
@ -703,13 +740,17 @@ class CuraEngineBackend(QObject, Backend):
application = Application.getInstance()
view = application.getController().getActiveView()
if view:
active_build_plate = application.getBuildPlateModel().activeBuildPlate
active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
self._layer_view_active = True
# There is data and we're not slicing at the moment
# if we are slicing, there is no need to re-calculate the data as it will be invalid in a moment.
# TODO: what build plate I am slicing
if active_build_plate in self._stored_optimized_layer_data and not self._slicing and not self._process_layers_job:
if (active_build_plate in self._stored_optimized_layer_data and
not self._slicing and
not self._process_layers_job and
active_build_plate not in self._build_plates_to_be_sliced):
self._startProcessSlicedLayersJob(active_build_plate)
else:
self._layer_view_active = False
@ -775,3 +816,9 @@ class CuraEngineBackend(QObject, Backend):
def tickle(self):
if self._use_timer:
self._change_timer.start()
def _extruderChanged(self):
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)
self._invokeSlice()

View file

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

View file

@ -81,7 +81,8 @@ class ProcessSlicedLayersJob(Job):
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
new_node = CuraSceneNode()
# The no_setting_override is here because adding the SettingOverrideDecorator will trigger a reslice
new_node = CuraSceneNode(no_setting_override = True)
new_node.addDecorator(BuildPlateDecorator(self._build_plate_number))
# Force garbage collection.

View file

@ -5,6 +5,7 @@ import numpy
from string import Formatter
from enum import IntEnum
import time
import re
from UM.Job import Job
from UM.Application import Application
@ -54,19 +55,19 @@ class GcodeStartEndFormatter(Formatter):
extruder_nr = int(kwargs["-1"][key_fragments[1]]) # get extruder_nr values from the global stack
except (KeyError, ValueError):
# either the key does not exist, or the value is not an int
Logger.log("w", "Unable to determine stack nr '%s' for key '%s' in start/end gcode, using global stack", key_fragments[1], key_fragments[0])
Logger.log("w", "Unable to determine stack nr '%s' for key '%s' in start/end g-code, using global stack", key_fragments[1], key_fragments[0])
elif len(key_fragments) != 1:
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end gcode", key)
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end g-code", key)
return "{" + str(key) + "}"
key = key_fragments[0]
try:
return kwargs[str(extruder_nr)][key]
except KeyError:
Logger.log("w", "Unable to replace '%s' placeholder in start/end gcode", key)
Logger.log("w", "Unable to replace '%s' placeholder in start/end g-code", key)
return "{" + key + "}"
else:
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end gcode", key)
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end g-code", key)
return "{" + str(key) + "}"
@ -128,16 +129,19 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.MaterialIncompatible)
return
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
for position, extruder_stack in stack.extruders.items():
material = extruder_stack.findContainer({"type": "material"})
if not extruder_stack.isEnabled:
continue
if material:
if material.getMetaDataEntry("compatible") == False:
self.setResult(StartJobResult.MaterialIncompatible)
return
# Don't slice if there is a per object setting with an error value.
for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is not CuraSceneNode or not node.isSelectable():
if not isinstance(node, CuraSceneNode) or not node.isSelectable():
continue
if self._checkStackForErrors(node.callDecoration("getStack")):
@ -169,7 +173,7 @@ class StartSliceJob(Job):
children = node.getAllChildren()
children.append(node)
for child_node in children:
if type(child_node) is CuraSceneNode and child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
if child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
temp_list.append(child_node)
if temp_list:
@ -181,17 +185,21 @@ class StartSliceJob(Job):
temp_list = []
has_printing_mesh = False
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("isSliceable") and type(node) is CuraSceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None:
per_object_stack = node.callDecoration("getStack")
is_non_printing_mesh = False
if per_object_stack:
is_non_printing_mesh = any(per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS)
if (node.callDecoration("getBuildPlateNumber") == self._build_plate_number):
if not getattr(node, "_outside_buildarea", False) or is_non_printing_mesh:
temp_list.append(node)
if not is_non_printing_mesh:
has_printing_mesh = True
# Find a reason not to add the node
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:
continue
if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh:
continue
temp_list.append(node)
if not is_non_printing_mesh:
has_printing_mesh = True
Job.yieldThread()
@ -203,10 +211,22 @@ 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()}
filtered_object_groups = []
for group in object_groups:
stack = Application.getInstance().getGlobalContainerStack()
skip_group = False
for node in group:
if not extruders_enabled[node.callDecoration("getActiveExtruderPosition")]:
skip_group = True
break
if not skip_group:
filtered_object_groups.append(group)
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
# the build volume)
if not object_groups:
if not filtered_object_groups:
self.setResult(StartJobResult.NothingToSlice)
return
@ -217,9 +237,9 @@ class StartSliceJob(Job):
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
self._buildExtruderMessage(extruder_stack)
for group in object_groups:
for group in filtered_object_groups:
group_message = self._slice_message.addRepeatedMessage("object_lists")
if group[0].getParent().callDecoration("isGroup"):
if group[0].getParent() is not None and group[0].getParent().callDecoration("isGroup"):
self._handlePerObjectSettings(group[0].getParent(), group_message)
for object in group:
mesh_data = object.getMeshData()
@ -267,9 +287,15 @@ class StartSliceJob(Job):
# \return A dictionary of replacement tokens to the values they should be
# replaced with.
def _buildReplacementTokens(self, stack) -> dict:
default_extruder_position = int(Application.getInstance().getMachineManager().defaultExtruderPosition)
result = {}
for key in stack.getAllKeys():
result[key] = stack.getProperty(key, "value")
setting_type = stack.definition.getProperty(key, "type")
value = stack.getProperty(key, "value")
if setting_type == "extruder" and value == -1:
# replace with the default value
value = default_extruder_position
result[key] = value
Job.yieldThread()
result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings.
@ -307,7 +333,7 @@ class StartSliceJob(Job):
settings["default_extruder_nr"] = default_extruder_nr
return str(fmt.format(value, **settings))
except:
Logger.logException("w", "Unable to do token replacement on start/end gcode")
Logger.logException("w", "Unable to do token replacement on start/end g-code")
return str(value)
## Create extruder message from stack
@ -343,10 +369,12 @@ class StartSliceJob(Job):
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
start_gcode = settings["machine_start_gcode"]
bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"}
settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings))
print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"}
settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings))
bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"]
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr}
settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None
print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"]
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr}
settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None
# Replace the setting tokens in start and end g-code.
# Use values from the first used extruder by default so we get the expected temperatures
@ -373,11 +401,11 @@ class StartSliceJob(Job):
# limit_to_extruder property.
def _buildGlobalInheritsStackMessage(self, stack):
for key in stack.getAllKeys():
extruder = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
if extruder >= 0: #Set to a specific extruder.
extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
if extruder_position >= 0: # Set to a specific extruder.
setting_extruder = self._slice_message.addRepeatedMessage("limit_to_extruder")
setting_extruder.name = key
setting_extruder.extruder = extruder
setting_extruder.extruder = extruder_position
Job.yieldThread()
## Check if a node has per object settings and ensure that they are set correctly in the message

View file

@ -20,7 +20,7 @@ i18n_catalog = i18nCatalog("cura")
# The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
# to change it to work for other applications.
class FirmwareUpdateChecker(Extension):
JEDI_VERSION_URL = "http://software.ultimaker.com/jedi/releases/latest.version"
JEDI_VERSION_URL = "http://software.ultimaker.com/jedi/releases/latest.version?utm_source=cura&utm_medium=software&utm_campaign=resources"
def __init__(self):
super().__init__()

View file

@ -69,7 +69,7 @@ class FirmwareUpdateCheckerJob(Job):
# If we do this in a cool way, the download url should be available in the JSON file
if self._set_download_url_callback:
self._set_download_url_callback("https://ultimaker.com/en/resources/20500-upgrade-firmware")
self._set_download_url_callback("https://ultimaker.com/en/resources/23129-updating-the-firmware?utm_source=cura&utm_medium=software&utm_campaign=hw-update")
message.actionTriggered.connect(self._callback)
message.show()

View file

@ -0,0 +1,33 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import gzip
import tempfile
from io import StringIO, BufferedIOBase #To write the g-code to a temporary buffer, and for typing.
from typing import List
from UM.Logger import Logger
from UM.Mesh.MeshReader import MeshReader #The class we're extending/implementing.
from UM.PluginRegistry import PluginRegistry
from UM.Scene.SceneNode import SceneNode #For typing.
## A file reader that reads gzipped g-code.
#
# If you're zipping g-code, you might as well use gzip!
class GCodeGzReader(MeshReader):
def __init__(self):
super().__init__()
self._supported_extensions = [".gcode.gz"]
def read(self, file_name):
with open(file_name, "rb") as file:
file_data = file.read()
uncompressed_gcode = gzip.decompress(file_data)
with tempfile.NamedTemporaryFile() as temp_file:
temp_file.write(uncompressed_gcode)
PluginRegistry.getInstance().getPluginObject("GCodeReader").preRead(temp_file.name)
result = PluginRegistry.getInstance().getPluginObject("GCodeReader").read(temp_file.name)
return result

View file

@ -0,0 +1,21 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import GCodeGzReader
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {
"mesh_reader": [
{
"extension": "gcode.gz",
"description": i18n_catalog.i18nc("@item:inlistbox", "Compressed G-code File")
}
]
}
def register(app):
app.addNonSliceableExtension(".gcode.gz")
return { "mesh_reader": GCodeGzReader.GCodeGzReader() }

View file

@ -0,0 +1,8 @@
{
"name": "Compressed G-code Reader",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Reads g-code from a compressed archive.",
"api": 4,
"i18n-catalog": "cura"
}

View file

@ -0,0 +1,41 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import gzip
from io import StringIO, BufferedIOBase #To write the g-code to a temporary buffer, and for typing.
from typing import List
from UM.Logger import Logger
from UM.Mesh.MeshWriter import MeshWriter #The class we're extending/implementing.
from UM.PluginRegistry import PluginRegistry
from UM.Scene.SceneNode import SceneNode #For typing.
## A file writer that writes gzipped g-code.
#
# If you're zipping g-code, you might as well use gzip!
class GCodeGzWriter(MeshWriter):
## Writes the gzipped g-code to a stream.
#
# Note that even though the function accepts a collection of nodes, the
# entire scene is always written to the file since it is not possible to
# separate the g-code for just specific nodes.
#
# \param stream The stream to write the gzipped g-code to.
# \param nodes This is ignored.
# \param mode Additional information on what type of stream to use. This
# must always be binary mode.
# \return Whether the write was successful.
def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode) -> bool:
if mode != MeshWriter.OutputMode.BinaryMode:
Logger.log("e", "GCodeGzWriter does not support text mode.")
return False
#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)
if not success: #Writing the g-code failed. Then I can also not write the gzipped g-code.
return False
result = gzip.compress(gcode_textio.getvalue().encode("utf-8"))
stream.write(result)
return True

View file

@ -0,0 +1,22 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import GCodeGzWriter
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {
"mesh_writer": {
"output": [{
"extension": "gcode.gz",
"description": catalog.i18nc("@item:inlistbox", "Compressed G-code File"),
"mime_type": "application/gzip",
"mode": GCodeGzWriter.GCodeGzWriter.OutputMode.BinaryMode
}]
}
}
def register(app):
return { "mesh_writer": GCodeGzWriter.GCodeGzWriter() }

View file

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

View file

@ -9,7 +9,7 @@ from UM.Logger import Logger
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from cura.ProfileReader import ProfileReader
from cura.ProfileReader import ProfileReader, NoProfileException
## A class that reads profile data from g-code files.
#
@ -66,12 +66,17 @@ class GCodeProfileReader(ProfileReader):
return None
serialized = unescapeGcodeComment(serialized)
serialized = serialized.strip()
if not serialized:
Logger.log("i", "No custom profile to import from this g-code: %s", file_name)
raise NoProfileException()
# serialized data can be invalid JSON
try:
json_data = json.loads(serialized)
except Exception as e:
Logger.log("e", "Could not parse serialized JSON data from GCode %s, error: %s", file_name, e)
Logger.log("e", "Could not parse serialized JSON data from g-code %s, error: %s", file_name, e)
return None
profiles = []

View file

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

View file

@ -437,7 +437,7 @@ 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().getBuildPlateModel().activeBuildPlate
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_dict = {active_build_plate_id: gcode_list}
Application.getInstance().getController().getScene().gcode_dict = gcode_dict

View file

@ -1,16 +1,17 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import re # For escaping characters in the settings.
import json
import copy
from UM.Mesh.MeshWriter import MeshWriter
from UM.Logger import Logger
from UM.Application import Application
from UM.Settings.InstanceContainer import InstanceContainer
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
import re #For escaping characters in the settings.
import json
import copy
## Writes g-code to a file.
#
@ -44,6 +45,8 @@ class GCodeWriter(MeshWriter):
def __init__(self):
super().__init__()
self._application = Application.getInstance()
## Writes the g-code for the entire scene to a stream.
#
# Note that even though the function accepts a collection of nodes, the
@ -56,10 +59,10 @@ class GCodeWriter(MeshWriter):
# file. This must always be text mode.
def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode):
if mode != MeshWriter.OutputMode.TextMode:
Logger.log("e", "GCode Writer does not support non-text mode.")
Logger.log("e", "GCodeWriter does not support non-text mode.")
return False
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
scene = Application.getInstance().getController().getScene()
gcode_dict = getattr(scene, "gcode_dict")
if not gcode_dict:
@ -93,7 +96,6 @@ class GCodeWriter(MeshWriter):
return flat_container
## Serialises a container stack to prepare it for writing at the end of the
# g-code.
#
@ -103,15 +105,20 @@ class GCodeWriter(MeshWriter):
# \param settings A container stack to serialise.
# \return A serialised string of the settings.
def _serialiseSettings(self, stack):
container_registry = self._application.getContainerRegistry()
quality_manager = self._application.getQualityManager()
prefix = ";SETTING_" + str(GCodeWriter.version) + " " # The prefix to put before each line.
prefix_length = len(prefix)
quality_type = stack.quality.getMetaDataEntry("quality_type")
container_with_profile = stack.qualityChanges
if container_with_profile.getId() == "empty_quality_changes":
Logger.log("e", "No valid quality profile found, not writing settings to GCode!")
return ""
# If the global quality changes is empty, create a new one
quality_name = container_registry.uniqueName(stack.quality.getName())
container_with_profile = quality_manager._createQualityChanges(quality_type, quality_name, stack, None)
flat_global_container = self._createFlattenedContainerInstance(stack.getTop(), container_with_profile)
flat_global_container = self._createFlattenedContainerInstance(stack.userChanges, container_with_profile)
# If the quality changes is not set, we need to set type manually
if flat_global_container.getMetaDataEntry("type", None) is None:
flat_global_container.addMetaDataEntry("type", "quality_changes")
@ -120,29 +127,47 @@ class GCodeWriter(MeshWriter):
if flat_global_container.getMetaDataEntry("quality_type", None) is None:
flat_global_container.addMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
# Get the machine definition ID for quality profiles
machine_definition_id_for_quality = getMachineDefinitionIDForQualitySearch(stack.definition)
flat_global_container.setMetaDataEntry("definition", machine_definition_id_for_quality)
serialized = flat_global_container.serialize()
data = {"global_quality": serialized}
for extruder in sorted(stack.extruders.values(), key = lambda k: k.getMetaDataEntry("position")):
all_setting_keys = set(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":
Logger.log("w", "No extruder quality profile found, not writing quality for extruder %s to file!", extruder.getId())
continue
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.getTop(), extruder_quality)
# Same story, if quality changes is empty, create a new one
quality_name = container_registry.uniqueName(stack.quality.getName())
extruder_quality = quality_manager._createQualityChanges(quality_type, quality_name, stack, None)
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.userChanges, extruder_quality)
# If the quality changes is not set, we need to set type manually
if flat_extruder_quality.getMetaDataEntry("type", None) is None:
flat_extruder_quality.addMetaDataEntry("type", "quality_changes")
# Ensure that extruder is set. (Can happen if we have empty quality changes).
if flat_extruder_quality.getMetaDataEntry("extruder", None) is None:
flat_extruder_quality.addMetaDataEntry("extruder", extruder.getBottom().getId())
if flat_extruder_quality.getMetaDataEntry("position", None) is None:
flat_extruder_quality.addMetaDataEntry("position", extruder.getMetaDataEntry("position"))
# Ensure that quality_type is set. (Can happen if we have empty quality changes).
if flat_extruder_quality.getMetaDataEntry("quality_type", None) is None:
flat_extruder_quality.addMetaDataEntry("quality_type", extruder.quality.getMetaDataEntry("quality_type", "normal"))
# Change the default definition
flat_extruder_quality.setMetaDataEntry("definition", machine_definition_id_for_quality)
extruder_serialized = flat_extruder_quality.serialize()
data.setdefault("extruder_quality", []).append(extruder_serialized)
all_setting_keys.update(set(flat_extruder_quality.getAllKeys()))
# Check if there is any profiles
if not all_setting_keys:
Logger.log("i", "No custom settings found, not writing settings to g-code.")
return ""
json_string = json.dumps(data)
# Escape characters that have a special meaning in g-code comments.
@ -156,5 +181,5 @@ class GCodeWriter(MeshWriter):
# Lines have 80 characters, so the payload of each line is 80 - prefix.
for pos in range(0, len(escaped_string), 80 - prefix_length):
result += prefix + escaped_string[pos : pos + 80 - prefix_length] + "\n"
result += prefix + escaped_string[pos: pos + 80 - prefix_length] + "\n"
return result

View file

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

View file

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

View file

@ -2,20 +2,16 @@
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal
import UM.i18n
from UM.FlameProfiler import pyqtSlot
from cura.MachineAction import MachineAction
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger
from cura.Settings.ExtruderManager import ExtruderManager
from cura.MachineAction import MachineAction
from cura.Settings.CuraStackBuilder import CuraStackBuilder
import UM.i18n
catalog = UM.i18n.i18nCatalog("cura")
@ -26,6 +22,8 @@ class MachineSettingsAction(MachineAction):
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
self._qml_url = "MachineSettingsAction.qml"
self._application = Application.getInstance()
self._global_container_stack = None
from cura.Settings.CuraContainerStack import _ContainerIndexes
@ -34,38 +32,44 @@ class MachineSettingsAction(MachineAction):
self._container_registry = ContainerRegistry.getInstance()
self._container_registry.containerAdded.connect(self._onContainerAdded)
self._container_registry.containerRemoved.connect(self._onContainerRemoved)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._empty_container = self._container_registry.getEmptyInstanceContainer()
self._backend = self._application.getBackend()
self._backend = Application.getInstance().getBackend()
self._empty_definition_container_id_list = []
def _isEmptyDefinitionChanges(self, container_id: str):
if not self._empty_definition_container_id_list:
self._empty_definition_container_id_list = [self._application.empty_container.getId(),
self._application.empty_definition_changes_container.getId()]
return container_id in self._empty_definition_container_id_list
def _onContainerAdded(self, container):
# Add this action as a supported action to all machine definitions
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
Application.getInstance().getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
def _onContainerRemoved(self, container):
# Remove definition_changes containers when a stack is removed
if container.getMetaDataEntry("type") in ["machine", "extruder_train"]:
definition_changes_container = container.definitionChanges
if definition_changes_container == self._empty_container:
definition_changes_id = container.definitionChanges.getId()
if self._isEmptyDefinitionChanges(definition_changes_id):
return
self._container_registry.removeContainer(definition_changes_container.getId())
self._container_registry.removeContainer(definition_changes_id)
def _reset(self):
if not self._global_container_stack:
return
# Make sure there is a definition_changes container to store the machine settings
definition_changes_container = self._global_container_stack.definitionChanges
if definition_changes_container == self._empty_container:
definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer(
self._global_container_stack, self._global_container_stack.getName() + "_settings")
definition_changes_id = self._global_container_stack.definitionChanges.getId()
if self._isEmptyDefinitionChanges(definition_changes_id):
CuraStackBuilder.createDefinitionChangesContainer(self._global_container_stack,
self._global_container_stack.getName() + "_settings")
# Notify the UI in which container to store the machine settings data
from cura.Settings.CuraContainerStack import CuraContainerStack, _ContainerIndexes
from cura.Settings.CuraContainerStack import _ContainerIndexes
container_index = _ContainerIndexes.DefinitionChanges
if container_index != self._container_index:
@ -107,13 +111,13 @@ class MachineSettingsAction(MachineAction):
def setMachineExtruderCount(self, extruder_count):
# Note: this method was in this class before, but since it's quite generic and other plugins also need it
# it was moved to the machine manager instead. Now this method just calls the machine manager.
Application.getInstance().getMachineManager().setActiveMachineExtruderCount(extruder_count)
self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count)
@pyqtSlot()
def forceUpdate(self):
# Force rebuilding the build volume by reloading the global container stack.
# This is a bit of a hack, but it seems quick enough.
Application.getInstance().globalContainerStackChanged.emit()
self._application.globalContainerStackChanged.emit()
@pyqtSlot()
def updateHasMaterialsMetadata(self):
@ -126,9 +130,11 @@ class MachineSettingsAction(MachineAction):
# In other words: only continue for the UM2 (extended), but not for the UM2+
return
stacks = ExtruderManager.getInstance().getExtruderStacks()
machine_manager = self._application.getMachineManager()
extruder_positions = list(self._global_container_stack.extruders.keys())
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
material_node = None
if has_materials:
if "has_materials" in self._global_container_stack.getMetaData():
self._global_container_stack.setMetaDataEntry("has_materials", True)
@ -136,26 +142,22 @@ class MachineSettingsAction(MachineAction):
self._global_container_stack.addMetaDataEntry("has_materials", True)
# Set the material container for each extruder to a sane default
for stack in stacks:
material_container = stack.material
if material_container == self._empty_container:
machine_approximate_diameter = str(round(self._global_container_stack.getProperty("material_diameter", "value")))
search_criteria = { "type": "material", "definition": "fdmprinter", "id": self._global_container_stack.getMetaDataEntry("preferred_material"), "approximate_diameter": machine_approximate_diameter}
materials = self._container_registry.findInstanceContainers(**search_criteria)
if materials:
stack.material = materials[0]
material_manager = self._application.getMaterialManager()
material_node = material_manager.getDefaultMaterial(self._global_container_stack, None)
else:
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
if "has_materials" in self._global_container_stack.getMetaData():
self._global_container_stack.removeMetaDataEntry("has_materials")
for stack in stacks:
stack.material = ContainerRegistry.getInstance().getEmptyInstanceContainer()
# set materials
for position in extruder_positions:
machine_manager.setMaterial(position, material_node)
Application.getInstance().globalContainerStackChanged.emit()
self._application.globalContainerStackChanged.emit()
@pyqtSlot(int)
def updateMaterialForDiameter(self, extruder_position: int):
# Updates the material container to a material that matches the material diameter set for the printer
Application.getInstance().getExtruderManager().updateMaterialForDiameter(extruder_position)
self._application.getExtruderManager().updateMaterialForDiameter(extruder_position)

View file

@ -70,8 +70,8 @@ Cura.MachineAction
anchors.top: pageTitle.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
property real columnWidth: ((width - 3 * UM.Theme.getSize("default_margin").width) / 2) | 0
property real labelColumnWidth: columnWidth * 0.5
property real columnWidth: Math.round((width - 3 * UM.Theme.getSize("default_margin").width) / 2)
property real labelColumnWidth: Math.round(columnWidth / 2)
Tab
{
@ -165,7 +165,7 @@ Cura.MachineAction
id: gcodeFlavorComboBox
sourceComponent: comboBoxWithOptions
property string settingKey: "machine_gcode_flavor"
property string label: catalog.i18nc("@label", "Gcode flavor")
property string label: catalog.i18nc("@label", "G-code flavor")
property bool forceUpdateOnChange: true
property var afterOnActivate: manager.updateHasMaterialsMetadata
}
@ -244,6 +244,7 @@ Cura.MachineAction
height: childrenRect.height
width: childrenRect.width
text: machineExtruderCountProvider.properties.description
visible: extruderCountModel.count >= 2
Row
{
@ -307,7 +308,7 @@ Cura.MachineAction
width: settingsTabs.columnWidth
Label
{
text: catalog.i18nc("@label", "Start Gcode")
text: catalog.i18nc("@label", "Start G-code")
font.bold: true
}
Loader
@ -317,7 +318,7 @@ Cura.MachineAction
property int areaWidth: parent.width
property int areaHeight: parent.height - y
property string settingKey: "machine_start_gcode"
property string tooltip: catalog.i18nc("@tooltip", "Gcode commands to be executed at the very start.")
property string tooltip: catalog.i18nc("@tooltip", "G-code commands to be executed at the very start.")
}
}
@ -326,7 +327,7 @@ Cura.MachineAction
width: settingsTabs.columnWidth
Label
{
text: catalog.i18nc("@label", "End Gcode")
text: catalog.i18nc("@label", "End G-code")
font.bold: true
}
Loader
@ -336,7 +337,7 @@ Cura.MachineAction
property int areaWidth: parent.width
property int areaHeight: parent.height - y
property string settingKey: "machine_end_gcode"
property string tooltip: catalog.i18nc("@tooltip", "Gcode commands to be executed at the very end.")
property string tooltip: catalog.i18nc("@tooltip", "G-code commands to be executed at the very end.")
}
}
}
@ -381,6 +382,11 @@ Cura.MachineAction
property string settingKey: "machine_nozzle_size"
property string label: catalog.i18nc("@label", "Nozzle size")
property string unit: catalog.i18nc("@label", "mm")
function afterOnEditingFinished()
{
// Somehow the machine_nozzle_size dependent settings are not updated otherwise
Cura.MachineManager.forceUpdateAllSettings()
}
property bool isExtruderSetting: true
}
@ -441,7 +447,7 @@ Cura.MachineAction
width: settingsTabs.columnWidth
Label
{
text: catalog.i18nc("@label", "Extruder Start Gcode")
text: catalog.i18nc("@label", "Extruder Start G-code")
font.bold: true
}
Loader
@ -459,7 +465,7 @@ Cura.MachineAction
width: settingsTabs.columnWidth
Label
{
text: catalog.i18nc("@label", "Extruder End Gcode")
text: catalog.i18nc("@label", "Extruder End G-code")
font.bold: true
}
Loader
@ -888,4 +894,4 @@ Cura.MachineAction
watchedProperties: [ "value" ]
storeIndex: manager.containerIndex
}
}
}

View file

@ -0,0 +1,116 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, pyqtProperty
from UM.Application import Application
from UM.Extension import Extension
from UM.Logger import Logger
from UM.Message import Message
from UM.i18n import i18nCatalog
from UM.PluginRegistry import PluginRegistry
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
catalog = i18nCatalog("cura")
class ModelChecker(QObject, Extension):
## Signal that gets emitted when anything changed that we need to check.
onChanged = pyqtSignal()
def __init__(self):
super().__init__()
self._button_view = None
self._caution_message = Message("", #Message text gets set when the message gets shown, to display the models in question.
lifetime = 0,
title = catalog.i18nc("@info:title", "Model Checker Warning"))
Application.getInstance().initializationFinished.connect(self._pluginsInitialized)
Application.getInstance().getController().getScene().sceneChanged.connect(self._onChanged)
## Pass-through to allow UM.Signal to connect with a pyqtSignal.
def _onChanged(self, _):
self.onChanged.emit()
## Called when plug-ins are initialized.
#
# This makes sure that we listen to changes of the material and that the
# button is created that indicates warnings with the current set-up.
def _pluginsInitialized(self):
Application.getInstance().getMachineManager().rootMaterialChanged.connect(self.onChanged)
self._createView()
def checkObjectsForShrinkage(self):
shrinkage_threshold = 0.5 #From what shrinkage percentage a warning will be issued about the model size.
warning_size_xy = 150 #The horizontal size of a model that would be too large when dealing with shrinking materials.
warning_size_z = 100 #The vertical size of a model that would be too large when dealing with shrinking materials.
material_shrinkage = self._getMaterialShrinkage()
warning_nodes = []
# Check node material shrinkage and bounding box size
for node in self.sliceableNodes():
node_extruder_position = node.callDecoration("getActiveExtruderPosition")
if material_shrinkage[node_extruder_position] > shrinkage_threshold:
bbox = node.getBoundingBox()
if bbox.width >= warning_size_xy or bbox.depth >= warning_size_xy or bbox.height >= warning_size_z:
warning_nodes.append(node)
self._caution_message.setText(catalog.i18nc(
"@info:status",
"Some models may not be printed optimal due to object size and chosen material for models: {model_names}.\n"
"Tips that may be useful to improve the print quality:\n"
"1) Use rounded corners\n"
"2) Turn the fan off (only if the are no tiny details on the model)\n"
"3) Use a different material").format(model_names = ", ".join([n.getName() for n in warning_nodes])))
return len(warning_nodes) > 0
def sliceableNodes(self):
# Add all sliceable scene nodes to check
scene = Application.getInstance().getController().getScene()
for node in DepthFirstIterator(scene.getRoot()):
if node.callDecoration("isSliceable"):
yield node
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
def _createView(self):
Logger.log("d", "Creating model checker view.")
# Create the plugin dialog component
path = os.path.join(PluginRegistry.getInstance().getPluginPath("ModelChecker"), "ModelChecker.qml")
self._button_view = Application.getInstance().createQmlComponent(path, {"manager": self})
# The qml is only the button
Application.getInstance().addAdditionalComponent("jobSpecsButton", self._button_view)
Logger.log("d", "Model checker view created.")
@pyqtProperty(bool, notify = onChanged)
def runChecks(self):
danger_shrinkage = self.checkObjectsForShrinkage()
return any((danger_shrinkage, )) #If any of the checks fail, show the warning button.
@pyqtSlot()
def showWarnings(self):
self._caution_message.show()
def _getMaterialShrinkage(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack is None:
return {}
material_shrinkage = {}
# Get all shrinkage values of materials used
for extruder_position, extruder in global_container_stack.extruders.items():
shrinkage = extruder.material.getProperty("material_shrinkage_percentage", "value")
if shrinkage is None:
shrinkage = 0
material_shrinkage[extruder_position] = shrinkage
return material_shrinkage

View file

@ -0,0 +1,43 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import UM 1.2 as UM
import Cura 1.0 as Cura
Button
{
id: modelCheckerButton
UM.I18nCatalog{id: catalog; name:"cura"}
visible: manager.runChecks
tooltip: catalog.i18nc("@info:tooltip", "Some things could be problematic in this print. Click to see tips for adjustment.")
onClicked: manager.showWarnings()
width: UM.Theme.getSize("save_button_specs_icons").width
height: UM.Theme.getSize("save_button_specs_icons").height
style: ButtonStyle
{
background: Item
{
UM.RecolorImage
{
width: UM.Theme.getSize("save_button_specs_icons").width;
height: UM.Theme.getSize("save_button_specs_icons").height;
sourceSize.width: width;
sourceSize.height: width;
color: control.hovered ? UM.Theme.getColor("text_scene_hover") : UM.Theme.getColor("text_scene");
source: "model_checker.svg"
}
}
}
}

View file

@ -0,0 +1,14 @@
# Copyright (c) 2018 Ultimaker B.V.
# This example is released under the terms of the AGPLv3 or higher.
from . import ModelChecker
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {}
def register(app):
return { "extension": ModelChecker.ModelChecker() }

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg">
<polygon fill="#000000" points="19 11 30 8 30 24 19 27" />
<path d="M10,19 C5.581722,19 2,15.418278 2,11 C2,6.581722 5.581722,3 10,3 C14.418278,3 18,6.581722 18,11 C18,15.418278 14.418278,19 10,19 Z M10,17 C13.3137085,17 16,14.3137085 16,11 C16,7.6862915 13.3137085,5 10,5 C6.6862915,5 4,7.6862915 4,11 C4,14.3137085 6.6862915,17 10,17 Z" fill="#000000" />
<polygon fill="#000000" points="4.2 15 6 16.8 1.8 21 0 19.2" />
<path d="M18.7333454,8.81666365 C18.2107269,6.71940704 16.9524304,4.91317986 15.248379,3.68790525 L18,3 L30,6 L18.7333454,8.81666365 Z M17,16.6573343 L17,27 L6,24 L6,19.0644804 C7.20495897,19.6632939 8.56315852,20 10,20 C12.8272661,20 15.3500445,18.6963331 17,16.6573343 Z" fill="#000000" />
</svg>

After

Width:  |  Height:  |  Size: 877 B

View file

@ -0,0 +1,8 @@
{
"name": "Model Checker",
"author": "Ultimaker B.V.",
"version": "0.1",
"api": 4,
"description": "Checks models and print configuration for possible printing issues and give suggestions.",
"i18n-catalog": "cura"
}

View file

@ -22,14 +22,7 @@ class MonitorStage(CuraStage):
def _setActivePrintJob(self, print_job):
if self._active_print_job != print_job:
if self._active_print_job:
self._active_print_job.stateChanged.disconnect(self._updateIconSource)
self._active_print_job = print_job
if self._active_print_job:
self._active_print_job.stateChanged.connect(self._updateIconSource)
# Ensure that the right icon source is returned.
self._updateIconSource()
def _setActivePrinter(self, printer):
if self._active_printer != printer:
@ -43,9 +36,6 @@ class MonitorStage(CuraStage):
else:
self._setActivePrintJob(None)
# Ensure that the right icon source is returned.
self._updateIconSource()
def _onActivePrintJobChanged(self):
self._setActivePrintJob(self._active_printer.activePrintJob)
@ -58,19 +48,16 @@ class MonitorStage(CuraStage):
new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
if new_output_device != self._printer_output_device:
if self._printer_output_device:
self._printer_output_device.acceptsCommandsChanged.disconnect(self._updateIconSource)
self._printer_output_device.connectionStateChanged.disconnect(self._updateIconSource)
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
try:
self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged)
except TypeError:
# Ignore stupid "Not connected" errors.
pass
self._printer_output_device = new_output_device
self._printer_output_device.acceptsCommandsChanged.connect(self._updateIconSource)
self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged)
self._printer_output_device.connectionStateChanged.connect(self._updateIconSource)
self._setActivePrinter(self._printer_output_device.activePrinter)
# Force an update of the icon source
self._updateIconSource()
except IndexError:
pass
@ -80,7 +67,6 @@ class MonitorStage(CuraStage):
self._onOutputDevicesChanged()
self._updateMainOverlay()
self._updateSidebar()
self._updateIconSource()
def _updateMainOverlay(self):
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"), "MonitorMainView.qml")
@ -90,46 +76,3 @@ class MonitorStage(CuraStage):
# TODO: currently the sidebar component for prepare and monitor stages is the same, this will change with the printer output device refactor!
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles), "Sidebar.qml")
self.addDisplayComponent("sidebar", sidebar_component_path)
def _updateIconSource(self):
if Application.getInstance().getTheme() is not None:
icon_name = self._getActiveOutputDeviceStatusIcon()
self.setIconSource(Application.getInstance().getTheme().getIcon(icon_name))
## Find the correct status icon depending on the active output device state
def _getActiveOutputDeviceStatusIcon(self):
# We assume that you are monitoring the device with the highest priority.
try:
output_device = Application.getInstance().getMachineManager().printerOutputDevices[0]
except IndexError:
return "tab_status_unknown"
if not output_device.acceptsCommands:
return "tab_status_unknown"
if output_device.activePrinter is None:
return "tab_status_connected"
# TODO: refactor to use enum instead of hardcoded strings?
if output_device.activePrinter.state == "maintenance":
return "tab_status_busy"
if output_device.activePrinter.activePrintJob is None:
return "tab_status_connected"
if output_device.activePrinter.activePrintJob.state in ["printing", "pre_print", "pausing", "resuming"]:
return "tab_status_busy"
if output_device.activePrinter.activePrintJob.state == "wait_cleanup":
return "tab_status_finished"
if output_device.activePrinter.activePrintJob.state in ["ready", ""]:
return "tab_status_connected"
if output_device.activePrinter.activePrintJob.state == "paused":
return "tab_status_paused"
if output_device.activePrinter.activePrintJob.state == "error":
return "tab_status_stopped"
return "tab_status_unknown"

View file

@ -163,7 +163,16 @@ Item {
id: addedSettingsModel;
containerId: Cura.MachineManager.activeDefinitionId
expanded: [ "*" ]
exclude: {
filter:
{
if (printSequencePropertyProvider.properties.value == "one_at_a_time")
{
return {"settable_per_meshgroup": true};
}
return {"settable_per_mesh": true};
}
exclude:
{
var excluded_settings = [ "support_mesh", "anti_overhang_mesh", "cutting_mesh", "infill_mesh" ];
if(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type == "support_mesh")
@ -237,7 +246,7 @@ Item {
Button
{
width: Math.floor(UM.Theme.getSize("setting").height / 2)
width: Math.round(UM.Theme.getSize("setting").height / 2)
height: UM.Theme.getSize("setting").height
onClicked: addedSettingsModel.setVisible(model.key, false)
@ -375,7 +384,6 @@ Item {
title: catalog.i18nc("@title:window", "Select Settings to Customize for this model")
width: screenScaleFactor * 360
property string labelFilter: ""
property var additional_excluded_settings
onVisibilityChanged:
@ -386,11 +394,33 @@ Item {
// Set skip setting, it will prevent from resetting selected mesh_type
contents.model.visibilityHandler.addSkipResetSetting(meshTypeSelection.model.get(meshTypeSelection.currentIndex).type)
listview.model.forceUpdate()
updateFilter()
}
}
function updateFilter()
{
var new_filter = {};
if (printSequencePropertyProvider.properties.value == "one_at_a_time")
{
new_filter["settable_per_meshgroup"] = true;
}
else
{
new_filter["settable_per_mesh"] = true;
}
if(filterInput.text != "")
{
new_filter["i18n_label"] = "*" + filterInput.text;
}
listview.model.filter = new_filter;
}
TextField {
id: filter
id: filterInput
anchors {
top: parent.top
@ -401,17 +431,7 @@ Item {
placeholderText: catalog.i18nc("@label:textbox", "Filter...");
onTextChanged:
{
if(text != "")
{
listview.model.filter = {"settable_per_mesh": true, "i18n_label": "*" + text}
}
else
{
listview.model.filter = {"settable_per_mesh": true}
}
}
onTextChanged: settingPickDialog.updateFilter()
}
CheckBox
@ -437,7 +457,7 @@ Item {
anchors
{
top: filter.bottom;
top: filterInput.bottom;
left: parent.left;
right: parent.right;
bottom: parent.bottom;
@ -449,10 +469,6 @@ Item {
{
id: definitionsModel;
containerId: Cura.MachineManager.activeDefinitionId
filter:
{
"settable_per_mesh": true
}
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
expanded: [ "*" ]
exclude:
@ -484,6 +500,7 @@ Item {
}
}
}
Component.onCompleted: settingPickDialog.updateFilter()
}
}
@ -507,6 +524,16 @@ Item {
storeIndex: 0
}
UM.SettingPropertyProvider
{
id: printSequencePropertyProvider
containerStackId: Cura.MachineManager.activeMachineId
key: "print_sequence"
watchedProperties: [ "value" ]
storeIndex: 0
}
SystemPalette { id: palette; }
Component

View file

@ -1,11 +1,10 @@
# Copyright (c) 2017 Ultimaker B.V.
# PluginBrowser is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QUrl, QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
from UM.Application import Application
from UM.Qt.ListModel import ListModel
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.Qt.Bindings.PluginsModel import PluginsModel
@ -20,7 +19,6 @@ import os
import tempfile
import platform
import zipfile
import shutil
from cura.CuraApplication import CuraApplication
@ -44,7 +42,7 @@ class PluginBrowser(QObject, Extension):
self._plugins_metadata = []
self._plugins_model = None
# Can be 'installed' or 'availble'
# Can be 'installed' or 'available'
self._view = "available"
self._restart_required = False
@ -287,8 +285,7 @@ class PluginBrowser(QObject, Extension):
@pyqtProperty(QObject, notify=pluginsMetadataChanged)
def pluginsModel(self):
print("Updating plugins model...", self._view)
self._plugins_model = PluginsModel(self._view)
self._plugins_model = PluginsModel(None, self._view)
# self._plugins_model.update()
# Check each plugin the registry for matching plugin from server
@ -367,7 +364,6 @@ class PluginBrowser(QObject, Extension):
# Add metadata to the manager:
self._plugins_metadata = json_data
print(self._plugins_metadata)
self._plugin_registry.addExternalPlugins(self._plugins_metadata)
self.pluginsMetadataChanged.emit()
except json.decoder.JSONDecodeError:

View file

@ -164,7 +164,7 @@ Component {
Button {
id: removeButton
text: "Uninstall"
visible: model.external && model.status == "installed"
visible: model.can_uninstall && model.status == "installed"
enabled: !manager.isDownloading
style: ButtonStyle {
background: Rectangle {

View file

@ -66,7 +66,7 @@ class PostProcessingPlugin(QObject, Extension):
return
# get gcode list for the active build plate
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
return
@ -117,50 +117,39 @@ class PostProcessingPlugin(QObject, Extension):
## Load all scripts from provided path.
# This should probably only be done on init.
# \param path Path to check for scripts.
def loadAllScripts(self):
def loadAllScripts(self, path):
if self._loaded_scripts: #Already loaded.
return
## Load all scripts in the scripts folders
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Preferences)]:
try:
path = os.path.join(root, "scripts")
if not os.path.isdir(path):
scripts = pkgutil.iter_modules(path = [path])
for loader, script_name, ispkg in scripts:
# Iterate over all scripts.
if script_name not in sys.modules:
try:
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py"))
loaded_script = importlib.util.module_from_spec(spec)
spec.loader.exec_module(loaded_script)
sys.modules[script_name] = loaded_script #TODO: This could be a security risk. Overwrite any module with a user-provided name?
loaded_class = getattr(loaded_script, script_name)
temp_object = loaded_class()
Logger.log("d", "Begin loading of script: %s", script_name)
try:
os.makedirs(path)
except OSError:
Logger.log("w", "Unable to create a folder for scripts: " + path)
continue
scripts = pkgutil.iter_modules(path = [path])
for loader, script_name, ispkg in scripts:
# Iterate over all scripts.
if script_name not in sys.modules:
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py"))
loaded_script = importlib.util.module_from_spec(spec)
spec.loader.exec_module(loaded_script)
sys.modules[script_name] = loaded_script #TODO: This could be a security risk. Overwrite any module with a user-provided name?
loaded_class = getattr(loaded_script, script_name)
temp_object = loaded_class()
Logger.log("d", "Begin loading of script: %s", script_name)
try:
setting_data = temp_object.getSettingData()
if "name" in setting_data and "key" in setting_data:
self._script_labels[setting_data["key"]] = setting_data["name"]
self._loaded_scripts[setting_data["key"]] = loaded_class
else:
Logger.log("w", "Script %s.py has no name or key", script_name)
self._script_labels[script_name] = script_name
self._loaded_scripts[script_name] = loaded_class
except AttributeError:
Logger.log("e", "Script %s.py is not a recognised script type. Ensure it inherits Script", script_name)
except NotImplementedError:
Logger.log("e", "Script %s.py has no implemented settings", script_name)
except Exception as e:
Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e)))
self.loadedScriptListChanged.emit()
setting_data = temp_object.getSettingData()
if "name" in setting_data and "key" in setting_data:
self._script_labels[setting_data["key"]] = setting_data["name"]
self._loaded_scripts[setting_data["key"]] = loaded_class
else:
Logger.log("w", "Script %s.py has no name or key", script_name)
self._script_labels[script_name] = script_name
self._loaded_scripts[script_name] = loaded_class
except AttributeError:
Logger.log("e", "Script %s.py is not a recognised script type. Ensure it inherits Script", script_name)
except NotImplementedError:
Logger.log("e", "Script %s.py has no implemented settings", script_name)
except Exception as e:
Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e)))
loadedScriptListChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = loadedScriptListChanged)
@ -189,7 +178,20 @@ class PostProcessingPlugin(QObject, Extension):
## When the global container stack is changed, swap out the list of active
# scripts.
def _onGlobalContainerStackChanged(self):
self.loadAllScripts() #Make sure we have all scripts if we didn't have them yet.
## Load all scripts in the scripts folders
# The PostProcessingPlugin path is for built-in scripts.
# The Resources path is where the user should store custom scripts.
# The Preferences path is legacy, where the user may previously have stored scripts.
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Resources), Resources.getStoragePath(Resources.Preferences)]:
path = os.path.join(root, "scripts")
if not os.path.isdir(path):
try:
os.makedirs(path)
except OSError:
Logger.log("w", "Unable to create a folder for scripts: " + path)
continue
self.loadAllScripts(path)
new_stack = Application.getInstance().getGlobalContainerStack()
self._script_list.clear()
if not new_stack.getMetaDataEntry("post_processing_scripts"): #Missing or empty.
@ -246,7 +248,20 @@ class PostProcessingPlugin(QObject, Extension):
def _createView(self):
Logger.log("d", "Creating post processing plugin view.")
self.loadAllScripts()
## Load all scripts in the scripts folders
# The PostProcessingPlugin path is for built-in scripts.
# The Resources path is where the user should store custom scripts.
# The Preferences path is legacy, where the user may previously have stored scripts.
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Resources), Resources.getStoragePath(Resources.Preferences)]:
path = os.path.join(root, "scripts")
if not os.path.isdir(path):
try:
os.makedirs(path)
except OSError:
Logger.log("w", "Unable to create a folder for scripts: " + path)
continue
self.loadAllScripts(path)
# Create the plugin dialog component
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")

View file

@ -33,8 +33,8 @@ UM.Dialog
{
UM.I18nCatalog{id: catalog; name:"cura"}
id: base
property int columnWidth: Math.floor((base.width / 2) - UM.Theme.getSize("default_margin").width)
property int textMargin: Math.floor(UM.Theme.getSize("default_margin").width / 2)
property int columnWidth: Math.round((base.width / 2) - UM.Theme.getSize("default_margin").width)
property int textMargin: Math.round(UM.Theme.getSize("default_margin").width / 2)
property string activeScriptName
SystemPalette{ id: palette }
SystemPalette{ id: disabledPalette; colorGroup: SystemPalette.Disabled }
@ -137,8 +137,8 @@ UM.Dialog
{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(control.width / 2.7)
height: Math.floor(control.height / 2.7)
width: Math.round(control.width / 2.7)
height: Math.round(control.height / 2.7)
sourceSize.width: width
sourceSize.height: width
color: palette.text
@ -172,8 +172,8 @@ UM.Dialog
{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(control.width / 2.5)
height: Math.floor(control.height / 2.5)
width: Math.round(control.width / 2.5)
height: Math.round(control.height / 2.5)
sourceSize.width: width
sourceSize.height: width
color: control.enabled ? palette.text : disabledPalette.text
@ -207,8 +207,8 @@ UM.Dialog
{
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(control.width / 2.5)
height: Math.floor(control.height / 2.5)
width: Math.round(control.width / 2.5)
height: Math.round(control.height / 2.5)
sourceSize.width: width
sourceSize.height: width
color: control.enabled ? palette.text : disabledPalette.text
@ -486,15 +486,15 @@ UM.Dialog
control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
Behavior on color { ColorAnimation { duration: 50; } }
anchors.left: parent.left
anchors.leftMargin: Math.floor(UM.Theme.getSize("save_button_text_margin").width / 2);
anchors.leftMargin: Math.round(UM.Theme.getSize("save_button_text_margin").width / 2);
width: parent.height
height: parent.height
UM.RecolorImage {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.floor(parent.width / 2)
height: Math.floor(parent.height / 2)
width: Math.round(parent.width / 2)
height: Math.round(parent.height / 2)
sourceSize.width: width
sourceSize.height: height
color: !control.enabled ? UM.Theme.getColor("action_button_disabled_text") :

View file

@ -105,6 +105,58 @@ class Script:
except:
return default
## Convenience function to produce a line of g-code.
#
# You can put in an original g-code line and it'll re-use all the values
# in that line.
# All other keyword parameters are put in the result in g-code's format.
# For instance, if you put ``G=1`` in the parameters, it will output
# ``G1``. If you put ``G=1, X=100`` in the parameters, it will output
# ``G1 X100``. The parameters G and M will always be put first. The
# parameters T and S will be put second (or first if there is no G or M).
# The rest of the parameters will be put in arbitrary order.
# \param line The original g-code line that must be modified. If not
# provided, an entirely new g-code line will be produced.
# \return A line of g-code with the desired parameters filled in.
def putValue(self, line = "", **kwargs):
#Strip the comment.
comment = ""
if ";" in line:
comment = line[line.find(";"):]
line = line[:line.find(";")] #Strip the comment.
#Parse the original g-code line.
for part in line.split(" "):
if part == "":
continue
parameter = part[0]
if parameter in kwargs:
continue #Skip this one. The user-provided parameter overwrites the one in the line.
value = part[1:]
kwargs[parameter] = value
#Write the new g-code line.
result = ""
priority_parameters = ["G", "M", "T", "S", "F", "X", "Y", "Z", "E"] #First some parameters that get priority. In order of priority!
for priority_key in priority_parameters:
if priority_key in kwargs:
if result != "":
result += " "
result += priority_key + str(kwargs[priority_key])
del kwargs[priority_key]
for key, value in kwargs.items():
if result != "":
result += " "
result += key + str(value)
#Put the comment back in.
if comment != "":
if result != "":
result += " "
result += ";" + comment
return result
## This is called when the script is executed.
# It gets a list of g-code strings and needs to return a (modified) list.
def execute(self, data):

View file

@ -1,10 +1,10 @@
# TweakAtZ script - Change printing parameters at a given height
# ChangeAtZ script - Change printing parameters at a given height
# This script is the successor of the TweakAtZ plugin for legacy Cura.
# It contains code from the TweakAtZ plugin V1.0-V4.x and from the ExampleScript by Jaime van Kessel, Ultimaker B.V.
# It runs with the PostProcessingPlugin which is released under the terms of the AGPLv3 or higher.
# This script is licensed under the Creative Commons - Attribution - Share Alike (CC BY-SA) terms
#Authors of the TweakAtZ plugin / script:
#Authors of the ChangeAtZ plugin / script:
# Written by Steven Morlock, smorloc@gmail.com
# Modified by Ricardo Gomez, ricardoga@otulook.com, to add Bed Temperature and make it work with Cura_13.06.04+
# Modified by Stefan Heule, Dim3nsioneer@gmx.ch since V3.0 (see changelog below)
@ -46,15 +46,15 @@ from ..Script import Script
#from UM.Logger import Logger
import re
class TweakAtZ(Script):
class ChangeAtZ(Script):
version = "5.1.1"
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"TweakAtZ """ + self.version + """ (Experimental)",
"key":"TweakAtZ",
"name":"ChangeAtZ """ + self.version + """ (Experimental)",
"key":"ChangeAtZ",
"metadata": {},
"version": 2,
"settings":
@ -69,8 +69,8 @@ class TweakAtZ(Script):
},
"b_targetZ":
{
"label": "Tweak Height",
"description": "Z height to tweak at",
"label": "Change Height",
"description": "Z height to change at",
"unit": "mm",
"type": "float",
"default_value": 5.0,
@ -81,8 +81,8 @@ class TweakAtZ(Script):
},
"b_targetL":
{
"label": "Tweak Layer",
"description": "Layer no. to tweak at",
"label": "Change Layer",
"description": "Layer no. to change at",
"unit": "",
"type": "int",
"default_value": 1,
@ -93,7 +93,7 @@ class TweakAtZ(Script):
"c_behavior":
{
"label": "Behavior",
"description": "Select behavior: Tweak value and keep it for the rest, Tweak value for single layer only",
"description": "Select behavior: Change value and keep it for the rest, Change value for single layer only",
"type": "enum",
"options": {"keep_value":"Keep value","single_layer":"Single Layer"},
"default_value": "keep_value"
@ -101,7 +101,7 @@ class TweakAtZ(Script):
"d_twLayers":
{
"label": "No. Layers",
"description": "No. of layers used to tweak",
"description": "No. of layers used to change",
"unit": "",
"type": "int",
"default_value": 1,
@ -109,10 +109,10 @@ class TweakAtZ(Script):
"maximum_value_warning": "50",
"enabled": "c_behavior == 'keep_value'"
},
"e1_Tweak_speed":
"e1_Change_speed":
{
"label": "Tweak Speed",
"description": "Select if total speed (print and travel) has to be tweaked",
"label": "Change Speed",
"description": "Select if total speed (print and travel) has to be cahnged",
"type": "bool",
"default_value": false
},
@ -126,12 +126,12 @@ class TweakAtZ(Script):
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "e1_Tweak_speed"
"enabled": "e1_Change_speed"
},
"f1_Tweak_printspeed":
"f1_Change_printspeed":
{
"label": "Tweak Print Speed",
"description": "Select if print speed has to be tweaked",
"label": "Change Print Speed",
"description": "Select if print speed has to be changed",
"type": "bool",
"default_value": false
},
@ -145,12 +145,12 @@ class TweakAtZ(Script):
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "f1_Tweak_printspeed"
"enabled": "f1_Change_printspeed"
},
"g1_Tweak_flowrate":
"g1_Change_flowrate":
{
"label": "Tweak Flow Rate",
"description": "Select if flow rate has to be tweaked",
"label": "Change Flow Rate",
"description": "Select if flow rate has to be changed",
"type": "bool",
"default_value": false
},
@ -164,12 +164,12 @@ class TweakAtZ(Script):
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "g1_Tweak_flowrate"
"enabled": "g1_Change_flowrate"
},
"g3_Tweak_flowrateOne":
"g3_Change_flowrateOne":
{
"label": "Tweak Flow Rate 1",
"description": "Select if first extruder flow rate has to be tweaked",
"label": "Change Flow Rate 1",
"description": "Select if first extruder flow rate has to be changed",
"type": "bool",
"default_value": false
},
@ -183,12 +183,12 @@ class TweakAtZ(Script):
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "g3_Tweak_flowrateOne"
"enabled": "g3_Change_flowrateOne"
},
"g5_Tweak_flowrateTwo":
"g5_Change_flowrateTwo":
{
"label": "Tweak Flow Rate 2",
"description": "Select if second extruder flow rate has to be tweaked",
"label": "Change Flow Rate 2",
"description": "Select if second extruder flow rate has to be changed",
"type": "bool",
"default_value": false
},
@ -202,12 +202,12 @@ class TweakAtZ(Script):
"minimum_value": "1",
"minimum_value_warning": "10",
"maximum_value_warning": "200",
"enabled": "g5_Tweak_flowrateTwo"
"enabled": "g5_Change_flowrateTwo"
},
"h1_Tweak_bedTemp":
"h1_Change_bedTemp":
{
"label": "Tweak Bed Temp",
"description": "Select if Bed Temperature has to be tweaked",
"label": "Change Bed Temp",
"description": "Select if Bed Temperature has to be changed",
"type": "bool",
"default_value": false
},
@ -221,12 +221,12 @@ class TweakAtZ(Script):
"minimum_value": "0",
"minimum_value_warning": "30",
"maximum_value_warning": "120",
"enabled": "h1_Tweak_bedTemp"
"enabled": "h1_Change_bedTemp"
},
"i1_Tweak_extruderOne":
"i1_Change_extruderOne":
{
"label": "Tweak Extruder 1 Temp",
"description": "Select if First Extruder Temperature has to be tweaked",
"label": "Change Extruder 1 Temp",
"description": "Select if First Extruder Temperature has to be changed",
"type": "bool",
"default_value": false
},
@ -240,12 +240,12 @@ class TweakAtZ(Script):
"minimum_value": "0",
"minimum_value_warning": "160",
"maximum_value_warning": "250",
"enabled": "i1_Tweak_extruderOne"
"enabled": "i1_Change_extruderOne"
},
"i3_Tweak_extruderTwo":
"i3_Change_extruderTwo":
{
"label": "Tweak Extruder 2 Temp",
"description": "Select if Second Extruder Temperature has to be tweaked",
"label": "Change Extruder 2 Temp",
"description": "Select if Second Extruder Temperature has to be changed",
"type": "bool",
"default_value": false
},
@ -259,12 +259,12 @@ class TweakAtZ(Script):
"minimum_value": "0",
"minimum_value_warning": "160",
"maximum_value_warning": "250",
"enabled": "i3_Tweak_extruderTwo"
"enabled": "i3_Change_extruderTwo"
},
"j1_Tweak_fanSpeed":
"j1_Change_fanSpeed":
{
"label": "Tweak Fan Speed",
"description": "Select if Fan Speed has to be tweaked",
"label": "Change Fan Speed",
"description": "Select if Fan Speed has to be changed",
"type": "bool",
"default_value": false
},
@ -278,17 +278,17 @@ class TweakAtZ(Script):
"minimum_value": "0",
"minimum_value_warning": "15",
"maximum_value_warning": "255",
"enabled": "j1_Tweak_fanSpeed"
"enabled": "j1_Change_fanSpeed"
}
}
}"""
def getValue(self, line, key, default = None): #replace default getvalue due to comment-reading feature
if not key in line or (";" in line and line.find(key) > line.find(";") and
not ";TweakAtZ" in key and not ";LAYER:" in key):
not ";ChangeAtZ" in key and not ";LAYER:" in key):
return default
subPart = line[line.find(key) + len(key):] #allows for string lengths larger than 1
if ";TweakAtZ" in key:
if ";ChangeAtZ" in key:
m = re.search("^[0-4]", subPart)
elif ";LAYER:" in key:
m = re.search("^[+-]?[0-9]*", subPart)
@ -303,17 +303,17 @@ class TweakAtZ(Script):
return default
def execute(self, data):
#Check which tweaks should apply
TweakProp = {"speed": self.getSettingValueByKey("e1_Tweak_speed"),
"flowrate": self.getSettingValueByKey("g1_Tweak_flowrate"),
"flowrateOne": self.getSettingValueByKey("g3_Tweak_flowrateOne"),
"flowrateTwo": self.getSettingValueByKey("g5_Tweak_flowrateTwo"),
"bedTemp": self.getSettingValueByKey("h1_Tweak_bedTemp"),
"extruderOne": self.getSettingValueByKey("i1_Tweak_extruderOne"),
"extruderTwo": self.getSettingValueByKey("i3_Tweak_extruderTwo"),
"fanSpeed": self.getSettingValueByKey("j1_Tweak_fanSpeed")}
TweakPrintSpeed = self.getSettingValueByKey("f1_Tweak_printspeed")
TweakStrings = {"speed": "M220 S%f\n",
#Check which changes should apply
ChangeProp = {"speed": self.getSettingValueByKey("e1_Change_speed"),
"flowrate": self.getSettingValueByKey("g1_Change_flowrate"),
"flowrateOne": self.getSettingValueByKey("g3_Change_flowrateOne"),
"flowrateTwo": self.getSettingValueByKey("g5_Change_flowrateTwo"),
"bedTemp": self.getSettingValueByKey("h1_Change_bedTemp"),
"extruderOne": self.getSettingValueByKey("i1_Change_extruderOne"),
"extruderTwo": self.getSettingValueByKey("i3_Change_extruderTwo"),
"fanSpeed": self.getSettingValueByKey("j1_Change_fanSpeed")}
ChangePrintSpeed = self.getSettingValueByKey("f1_Change_printspeed")
ChangeStrings = {"speed": "M220 S%f\n",
"flowrate": "M221 S%f\n",
"flowrateOne": "M221 T0 S%f\n",
"flowrateTwo": "M221 T1 S%f\n",
@ -369,14 +369,14 @@ class TweakAtZ(Script):
for line in lines:
if ";Generated with Cura_SteamEngine" in line:
TWinstances += 1
modified_gcode += ";TweakAtZ instances: %d\n" % TWinstances
if not ("M84" in line or "M25" in line or ("G1" in line and TweakPrintSpeed and (state==3 or state==4)) or
";TweakAtZ instances:" in line):
modified_gcode += ";ChangeAtZ instances: %d\n" % TWinstances
if not ("M84" in line or "M25" in line or ("G1" in line and ChangePrintSpeed and (state==3 or state==4)) or
";ChangeAtZ instances:" in line):
modified_gcode += line + "\n"
IsUM2 = ("FLAVOR:UltiGCode" in line) or IsUM2 #Flavor is UltiGCode!
if ";TweakAtZ-state" in line: #checks for state change comment
state = self.getValue(line, ";TweakAtZ-state", state)
if ";TweakAtZ instances:" in line:
if ";ChangeAtZ-state" in line: #checks for state change comment
state = self.getValue(line, ";ChangeAtZ-state", state)
if ";ChangeAtZ instances:" in line:
try:
tempTWi = int(line[20:])
except:
@ -390,7 +390,7 @@ class TweakAtZ(Script):
state = old["state"]
layer = self.getValue(line, ";LAYER:", layer)
if targetL_i > -100000: #target selected by layer no.
if (state == 2 or targetL_i == 0) and layer == targetL_i: #determine targetZ from layer no.; checks for tweak on layer 0
if (state == 2 or targetL_i == 0) and layer == targetL_i: #determine targetZ from layer no.; checks for change on layer 0
state = 2
targetZ = z + 0.001
if (self.getValue(line, "T", None) is not None) and (self.getValue(line, "M", None) is None): #looking for single T-cmd
@ -415,7 +415,7 @@ class TweakAtZ(Script):
elif tmp_extruder == 1: #second extruder
old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"])
if ("M84" in line or "M25" in line):
if state>0 and TweakProp["speed"]: #"finish" commands for UM Original and UM2
if state>0 and ChangeProp["speed"]: #"finish" commands for UM Original and UM2
modified_gcode += "M220 S100 ; speed reset to 100% at the end of print\n"
modified_gcode += "M117 \n"
modified_gcode += line + "\n"
@ -425,14 +425,14 @@ class TweakAtZ(Script):
y = self.getValue(line, "Y", None)
e = self.getValue(line, "E", None)
f = self.getValue(line, "F", None)
if 'G1' in line and TweakPrintSpeed and (state==3 or state==4):
if 'G1' in line and ChangePrintSpeed and (state==3 or state==4):
# check for pure print movement in target range:
if x != None and y != None and f != None and e != None and newZ==z:
modified_gcode += "G1 F%d X%1.3f Y%1.3f E%1.5f\n" % (int(f / 100.0 * float(target_values["printspeed"])), self.getValue(line, "X"),
self.getValue(line, "Y"), self.getValue(line, "E"))
else: #G1 command but not a print movement
modified_gcode += line + "\n"
# no tweaking on retraction hops which have no x and y coordinate:
# no changing on retraction hops which have no x and y coordinate:
if (newZ != z) and (x is not None) and (y is not None):
z = newZ
if z < targetZ and state == 1:
@ -440,56 +440,56 @@ class TweakAtZ(Script):
if z >= targetZ and state == 2:
state = 3
done_layers = 0
for key in TweakProp:
if TweakProp[key] and old[key]==-1: #old value is not known
for key in ChangeProp:
if ChangeProp[key] and old[key]==-1: #old value is not known
oldValueUnknown = True
if oldValueUnknown: #the tweaking has to happen within one layer
if oldValueUnknown: #the changing has to happen within one layer
twLayers = 1
if IsUM2: #Parameters have to be stored in the printer (UltiGCode=UM2)
modified_gcode += "M605 S%d;stores parameters before tweaking\n" % (TWinstances-1)
if behavior == 1: #single layer tweak only and then reset
modified_gcode += "M605 S%d;stores parameters before changing\n" % (TWinstances-1)
if behavior == 1: #single layer change only and then reset
twLayers = 1
if TweakPrintSpeed and behavior == 0:
if ChangePrintSpeed and behavior == 0:
twLayers = done_layers + 1
if state==3:
if twLayers-done_layers>0: #still layers to go?
if targetL_i > -100000:
modified_gcode += ";TweakAtZ V%s: executed at Layer %d\n" % (self.version,layer)
modified_gcode += "M117 Printing... tw@L%4d\n" % layer
modified_gcode += ";ChangeAtZ V%s: executed at Layer %d\n" % (self.version,layer)
modified_gcode += "M117 Printing... ch@L%4d\n" % layer
else:
modified_gcode += (";TweakAtZ V%s: executed at %1.2f mm\n" % (self.version,z))
modified_gcode += "M117 Printing... tw@%5.1f\n" % z
for key in TweakProp:
if TweakProp[key]:
modified_gcode += TweakStrings[key] % float(old[key]+(float(target_values[key])-float(old[key]))/float(twLayers)*float(done_layers+1))
modified_gcode += (";ChangeAtZ V%s: executed at %1.2f mm\n" % (self.version,z))
modified_gcode += "M117 Printing... ch@%5.1f\n" % z
for key in ChangeProp:
if ChangeProp[key]:
modified_gcode += ChangeStrings[key] % float(old[key]+(float(target_values[key])-float(old[key]))/float(twLayers)*float(done_layers+1))
done_layers += 1
else:
state = 4
if behavior == 1: #reset values after one layer
if targetL_i > -100000:
modified_gcode += ";TweakAtZ V%s: reset on Layer %d\n" % (self.version,layer)
modified_gcode += ";ChangeAtZ V%s: reset on Layer %d\n" % (self.version,layer)
else:
modified_gcode += ";TweakAtZ V%s: reset at %1.2f mm\n" % (self.version,z)
modified_gcode += ";ChangeAtZ V%s: reset at %1.2f mm\n" % (self.version,z)
if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting
modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1)
else: #executes on RepRap, UM2 with Ultigcode and Cura setting
for key in TweakProp:
if TweakProp[key]:
modified_gcode += TweakStrings[key] % float(old[key])
for key in ChangeProp:
if ChangeProp[key]:
modified_gcode += ChangeStrings[key] % float(old[key])
# re-activates the plugin if executed by pre-print G-command, resets settings:
if (z < targetZ or layer == 0) and state >= 3: #resets if below tweak level or at level 0
if (z < targetZ or layer == 0) and state >= 3: #resets if below change level or at level 0
state = 2
done_layers = 0
if targetL_i > -100000:
modified_gcode += ";TweakAtZ V%s: reset below Layer %d\n" % (self.version,targetL_i)
modified_gcode += ";ChangeAtZ V%s: reset below Layer %d\n" % (self.version,targetL_i)
else:
modified_gcode += ";TweakAtZ V%s: reset below %1.2f mm\n" % (self.version,targetZ)
modified_gcode += ";ChangeAtZ V%s: reset below %1.2f mm\n" % (self.version,targetZ)
if IsUM2 and oldValueUnknown: #executes on UM2 with Ultigcode and machine setting
modified_gcode += "M606 S%d;recalls saved settings\n" % (TWinstances-1)
else: #executes on RepRap, UM2 with Ultigcode and Cura setting
for key in TweakProp:
if TweakProp[key]:
modified_gcode += TweakStrings[key] % float(old[key])
for key in ChangeProp:
if ChangeProp[key]:
modified_gcode += ChangeStrings[key] % float(old[key])
data[index] = modified_gcode
index += 1
return data

View file

@ -2,17 +2,15 @@
# under the terms of the AGPLv3 or higher
from ..Script import Script
#from UM.Logger import Logger
# from cura.Settings.ExtruderManager import ExtruderManager
class ColorChange(Script):
class FilamentChange(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Color Change",
"key": "ColorChange",
"name":"Filament Change",
"key": "FilamentChange",
"metadata": {},
"version": 2,
"settings":
@ -29,18 +27,18 @@ class ColorChange(Script):
"initial_retract":
{
"label": "Initial Retraction",
"description": "Initial filament retraction distance",
"description": "Initial filament retraction distance. The filament will be retracted with this amount before moving the nozzle away from the ongoing print.",
"unit": "mm",
"type": "float",
"default_value": 300.0
"default_value": 30.0
},
"later_retract":
{
"label": "Later Retraction Distance",
"description": "Later filament retraction distance for removal",
"description": "Later filament retraction distance for removal. The filament will be retracted all the way out of the printer so that you can change the filament.",
"unit": "mm",
"type": "float",
"default_value": 30.0
"default_value": 300.0
}
}
}"""
@ -60,17 +58,17 @@ class ColorChange(Script):
if later_retract is not None and later_retract > 0.:
color_change = color_change + (" L%.2f" % later_retract)
color_change = color_change + " ; Generated by ColorChange plugin"
color_change = color_change + " ; Generated by FilamentChange plugin"
layer_targets = layer_nums.split(',')
layer_targets = layer_nums.split(",")
if len(layer_targets) > 0:
for layer_num in layer_targets:
layer_num = int( layer_num.strip() )
layer_num = int(layer_num.strip())
if layer_num < len(data):
layer = data[ layer_num - 1 ]
layer = data[layer_num - 1]
lines = layer.split("\n")
lines.insert(2, color_change )
final_line = "\n".join( lines )
data[ layer_num - 1 ] = final_line
lines.insert(2, color_change)
final_line = "\n".join(lines)
data[layer_num - 1] = final_line
return data

View file

@ -7,19 +7,40 @@ class PauseAtHeight(Script):
def getSettingDataString(self):
return """{
"name":"Pause at height",
"name": "Pause at height",
"key": "PauseAtHeight",
"metadata": {},
"version": 2,
"settings":
{
"pause_at":
{
"label": "Pause at",
"description": "Whether to pause at a certain height or at a certain layer.",
"type": "enum",
"options": {"height": "Height", "layer_no": "Layer No."},
"default_value": "height"
},
"pause_height":
{
"label": "Pause Height",
"description": "At what height should the pause occur",
"unit": "mm",
"type": "float",
"default_value": 5.0
"default_value": 5.0,
"minimum_value": "0",
"minimum_value_warning": "0.27",
"enabled": "pause_at == 'height'"
},
"pause_layer":
{
"label": "Pause Layer",
"description": "At what layer should the pause occur",
"type": "int",
"value": "math.floor((pause_height - 0.27) / 0.1) + 1",
"minimum_value": "0",
"minimum_value_warning": "1",
"enabled": "pause_at == 'layer_no'"
},
"head_park_x":
{
@ -102,8 +123,9 @@ class PauseAtHeight(Script):
x = 0.
y = 0.
current_z = 0.
pause_at = self.getSettingValueByKey("pause_at")
pause_height = self.getSettingValueByKey("pause_height")
pause_layer = self.getSettingValueByKey("pause_layer")
retraction_amount = self.getSettingValueByKey("retraction_amount")
retraction_speed = self.getSettingValueByKey("retraction_speed")
extrude_amount = self.getSettingValueByKey("extrude_amount")
@ -121,101 +143,133 @@ class PauseAtHeight(Script):
# use offset to calculate the current height: <current_height> = <current_z> - <layer_0_z>
layer_0_z = 0.
current_z = 0
got_first_g_cmd_on_layer_0 = False
for layer in data:
for index, layer in enumerate(data):
lines = layer.split("\n")
for line in lines:
if ";LAYER:0" in line:
layers_started = True
continue
if not layers_started:
continue
if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
current_z = self.getValue(line, 'Z')
if self.getValue(line, "Z") is not None:
current_z = self.getValue(line, "Z")
if pause_at == "height":
if self.getValue(line, "G") != 1 and self.getValue(line, "G") != 0:
continue
if not got_first_g_cmd_on_layer_0:
layer_0_z = current_z
got_first_g_cmd_on_layer_0 = True
x = self.getValue(line, 'X', x)
y = self.getValue(line, 'Y', y)
if current_z is not None:
current_height = current_z - layer_0_z
if current_height >= pause_height:
index = data.index(layer)
prevLayer = data[index - 1]
prevLines = prevLayer.split("\n")
current_e = 0.
for prevLine in reversed(prevLines):
current_e = self.getValue(prevLine, 'E', -1)
if current_e >= 0:
break
x = self.getValue(line, "X", x)
y = self.getValue(line, "Y", y)
# include a number of previous layers
for i in range(1, redo_layers + 1):
prevLayer = data[index - i]
layer = prevLayer + layer
current_height = current_z - layer_0_z
if current_height < pause_height:
break #Try the next layer.
else: #Pause at layer.
if not line.startswith(";LAYER:"):
continue
current_layer = line[len(";LAYER:"):]
try:
current_layer = int(current_layer)
except ValueError: #Couldn't cast to int. Something is wrong with this g-code data.
continue
if current_layer < pause_layer:
break #Try the next layer.
prepend_gcode = ";TYPE:CUSTOM\n"
prepend_gcode += ";added code by post processing\n"
prepend_gcode += ";script: PauseAtHeight.py\n"
prepend_gcode += ";current z: %f \n" % current_z
prepend_gcode += ";current height: %f \n" % current_height
prevLayer = data[index - 1]
prevLines = prevLayer.split("\n")
current_e = 0.
# Retraction
prepend_gcode += "M83\n"
if retraction_amount != 0:
prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60)
# Move the head away
prepend_gcode += "G1 Z%f F300\n" % (current_z + 1)
prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y)
if current_z < 15:
prepend_gcode += "G1 Z15 F300\n"
# Disable the E steppers
prepend_gcode += "M84 E0\n"
# Set extruder standby temperature
prepend_gcode += "M104 S%i; standby temperature\n" % (standby_temperature)
# Wait till the user continues printing
prepend_gcode += "M0 ;Do the actual pause\n"
# Set extruder resume temperature
prepend_gcode += "M109 S%i; resume temperature\n" % (resume_temperature)
# Push the filament back,
if retraction_amount != 0:
prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60)
# Optionally extrude material
if extrude_amount != 0:
prepend_gcode += "G1 E%f F%f\n" % (extrude_amount, extrude_speed * 60)
# and retract again, the properly primes the nozzle
# when changing filament.
if retraction_amount != 0:
prepend_gcode += "G1 E-%f F%f\n" % (retraction_amount, retraction_speed * 60)
# Move the head back
prepend_gcode += "G1 Z%f F300\n" % (current_z + 1)
prepend_gcode += "G1 X%f Y%f F9000\n" % (x, y)
if retraction_amount != 0:
prepend_gcode += "G1 E%f F%f\n" % (retraction_amount, retraction_speed * 60)
prepend_gcode += "G1 F9000\n"
prepend_gcode += "M82\n"
# reset extrude value to pre pause value
prepend_gcode += "G92 E%f\n" % (current_e)
layer = prepend_gcode + layer
# Override the data of this layer with the
# modified data
data[index] = layer
return data
# Access last layer, browse it backwards to find
# last extruder absolute position
for prevLine in reversed(prevLines):
current_e = self.getValue(prevLine, "E", -1)
if current_e >= 0:
break
# include a number of previous layers
for i in range(1, redo_layers + 1):
prevLayer = data[index - i]
layer = prevLayer + layer
# Get extruder's absolute position at the
# begining of the first layer redone
# see https://github.com/nallath/PostProcessingPlugin/issues/55
if i == redo_layers:
prevLines = prevLayer.split("\n")
for line in prevLines:
new_e = self.getValue(line, 'E', current_e)
if new_e != current_e:
current_e = new_e
break
prepend_gcode = ";TYPE:CUSTOM\n"
prepend_gcode += ";added code by post processing\n"
prepend_gcode += ";script: PauseAtHeight.py\n"
if pause_at == "height":
prepend_gcode += ";current z: {z}\n".format(z = current_z)
prepend_gcode += ";current height: {height}\n".format(height = current_height)
else:
prepend_gcode += ";current layer: {layer}\n".format(layer = current_layer)
# Retraction
prepend_gcode += self.putValue(M = 83) + "\n"
if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
# Move the head away
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n"
prepend_gcode += self.putValue(G = 1, X = park_x, Y = park_y, F = 9000) + "\n"
if current_z < 15:
prepend_gcode += self.putValue(G = 1, Z = 15, F = 300) + "\n"
# Disable the E steppers
prepend_gcode += self.putValue(M = 84, E = 0) + "\n"
# Set extruder standby temperature
prepend_gcode += self.putValue(M = 104, S = standby_temperature) + "; standby temperature\n"
# Wait till the user continues printing
prepend_gcode += self.putValue(M = 0) + ";Do the actual pause\n"
# Set extruder resume temperature
prepend_gcode += self.putValue(M = 109, S = resume_temperature) + "; resume temperature\n"
# Push the filament back,
if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n"
# Optionally extrude material
if extrude_amount != 0:
prepend_gcode += self.putValue(G = 1, E = extrude_amount, F = extrude_speed * 60) + "\n"
# and retract again, the properly primes the nozzle
# when changing filament.
if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
# Move the head back
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n"
prepend_gcode += self.putValue(G = 1, X = x, Y = y, F = 9000) + "\n"
if retraction_amount != 0:
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n"
prepend_gcode += self.putValue(G = 1, F = 9000) + "\n"
prepend_gcode += self.putValue(M = 82) + "\n"
# reset extrude value to pre pause value
prepend_gcode += self.putValue(G = 92, E = current_e) + "\n"
layer = prepend_gcode + layer
# Override the data of this layer with the
# modified data
data[index] = layer
return data
return data

View file

@ -35,7 +35,7 @@ class PauseAtHeightforRepetier(Script):
"type": "float",
"default_value": 5.0
},
"head_move_Z":
"head_move_Z":
{
"label": "Head move Z",
"description": "The Hieght of Z-axis retraction before parking.",

View file

@ -12,6 +12,7 @@ import numpy as np
from UM.Logger import Logger
from UM.Application import Application
import re
from cura.Settings.ExtruderManager import ExtruderManager
def _getValue(line, key, default=None):
"""
@ -90,9 +91,9 @@ class Stretcher():
"""
Computes the new X and Y coordinates of all g-code steps
"""
Logger.log("d", "Post stretch with line width = " + str(self.line_width)
+ "mm wide circle stretch = " + str(self.wc_stretch)+ "mm"
+ "and push wall stretch = " + str(self.pw_stretch) + "mm")
Logger.log("d", "Post stretch with line width " + str(self.line_width)
+ "mm wide circle stretch " + str(self.wc_stretch)+ "mm"
+ " and push wall stretch " + str(self.pw_stretch) + "mm")
retdata = []
layer_steps = []
current = GCodeStep(0)
@ -282,7 +283,7 @@ class Stretcher():
dmin_tri is the minimum distance between two consecutive points
of an acceptable triangle
"""
dmin_tri = self.line_width / 2.0
dmin_tri = 0.5
iextra_base = np.floor_divide(len(orig_seq), 3) # Nb of extra points
ibeg = 0 # Index of first point of the triangle
iend = 0 # Index of the third point of the triangle
@ -325,9 +326,10 @@ class Stretcher():
relpos = 0.5 # To avoid division by zero or precision loss
projection = (pos_before[ibeg] + relpos * (pos_after[iend] - pos_before[ibeg]))
dist_from_proj = np.sqrt(((projection - step) ** 2).sum(0))
if dist_from_proj > 0.001: # Move central point only if points are not aligned
if dist_from_proj > 0.0003: # Move central point only if points are not aligned
modif_seq[i] = (step - (self.wc_stretch / dist_from_proj)
* (projection - step))
return
def wideTurn(self, orig_seq, modif_seq):
@ -411,8 +413,6 @@ class Stretcher():
modif_seq[ibeg] = modif_seq[ibeg] + xperp * self.pw_stretch
elif not materialleft and materialright:
modif_seq[ibeg] = modif_seq[ibeg] - xperp * self.pw_stretch
if materialleft and materialright:
modif_seq[ibeg] = orig_seq[ibeg] # Surrounded by walls, don't move
# Setup part of the stretch plugin
class Stretch(Script):
@ -437,7 +437,7 @@ class Stretch(Script):
"description": "Distance by which the points are moved by the correction effect in corners. The higher this value, the higher the effect",
"unit": "mm",
"type": "float",
"default_value": 0.08,
"default_value": 0.1,
"minimum_value": 0,
"minimum_value_warning": 0,
"maximum_value_warning": 0.2
@ -448,7 +448,7 @@ class Stretch(Script):
"description": "Distance by which the points are moved by the correction effect when two lines are nearby. The higher this value, the higher the effect",
"unit": "mm",
"type": "float",
"default_value": 0.08,
"default_value": 0.1,
"minimum_value": 0,
"minimum_value_warning": 0,
"maximum_value_warning": 0.2
@ -463,7 +463,7 @@ class Stretch(Script):
the returned string is the list of modified g-code instructions
"""
stretcher = Stretcher(
Application.getInstance().getGlobalContainerStack().getProperty("line_width", "value")
ExtruderManager.getInstance().getActiveExtruderStack().getProperty("machine_nozzle_size", "value")
, self.getSettingValueByKey("wc_stretch"), self.getSettingValueByKey("pw_stretch"))
return stretcher.execute(data)

View file

@ -1,4 +1,4 @@
# Copyright (c) 2016 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
@ -7,7 +7,7 @@ from UM.Application import Application
from UM.Logger import Logger
from UM.Message import Message
from UM.FileHandler.WriteFileJob import WriteFileJob
from UM.Mesh.MeshWriter import MeshWriter
from UM.FileHandler.FileWriter import FileWriter #To check against the write modes (text vs. binary).
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.OutputDevice.OutputDevice import OutputDevice
from UM.OutputDevice import OutputDeviceError
@ -39,7 +39,7 @@ class RemovableDriveOutputDevice(OutputDevice):
# MIME types available to the currently active machine?
#
def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs):
filter_by_machine = True # This plugin is indended to be used by machine (regardless of what it was told to do)
filter_by_machine = True # This plugin is intended to be used by machine (regardless of what it was told to do)
if self._writing:
raise OutputDeviceError.DeviceBusyError()
@ -56,19 +56,21 @@ class RemovableDriveOutputDevice(OutputDevice):
machine_file_formats = [file_type.strip() for file_type in container.getMetaDataEntry("file_formats").split(";")]
# Take the intersection between file_formats and machine_file_formats.
file_formats = list(filter(lambda file_format: file_format["mime_type"] in machine_file_formats, file_formats))
format_by_mimetype = {format["mime_type"]: format for format in file_formats}
file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats.
if len(file_formats) == 0:
Logger.log("e", "There are no file formats available to write with!")
raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("There are no file formats available to write with!"))
raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status", "There are no file formats available to write with!"))
preferred_format = file_formats[0]
# Just take the first file format available.
if file_handler is not None:
writer = file_handler.getWriterByMimeType(file_formats[0]["mime_type"])
writer = file_handler.getWriterByMimeType(preferred_format["mime_type"])
else:
writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(file_formats[0]["mime_type"])
writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(preferred_format["mime_type"])
extension = file_formats[0]["extension"]
extension = preferred_format["extension"]
if file_name is None:
file_name = self._automaticFileName(nodes)
@ -80,8 +82,11 @@ class RemovableDriveOutputDevice(OutputDevice):
try:
Logger.log("d", "Writing to %s", file_name)
# Using buffering greatly reduces the write time for many lines of gcode
self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8")
job = WriteFileJob(writer, self._stream, nodes, MeshWriter.OutputMode.TextMode)
if preferred_format["mode"] == FileWriter.OutputMode.TextMode:
self._stream = open(file_name, "wt", buffering = 1, encoding = "utf-8")
else: #Binary mode.
self._stream = open(file_name, "wb", buffering = 1)
job = WriteFileJob(writer, self._stream, nodes, preferred_format["mode"])
job.setFileName(file_name)
job.progress.connect(self._onProgress)
job.finished.connect(self._onFinished)

View file

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

View file

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

View file

@ -74,7 +74,7 @@ class SimulationView(View):
self._global_container_stack = None
self._proxy = SimulationViewProxy()
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
self._controller.getScene().sceneChanged.connect(self._onSceneChanged)
self._resetSettings()
self._legend_items = None
@ -158,9 +158,12 @@ class SimulationView(View):
return self._nozzle_node
def _onSceneChanged(self, node):
self.setActivity(False)
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)
if node.getMeshData() is None:
self.resetLayerData()
else:
self.setActivity(False)
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)
def isBusy(self):
return self._busy
@ -342,6 +345,11 @@ class SimulationView(View):
min_layer_number = sys.maxsize
max_layer_number = -sys.maxsize
for layer_id in layer_data.getLayers():
# If a layer doesn't contain any polygons, skip it (for infill meshes taller than print objects
if len(layer_data.getLayer(layer_id).polygons) < 1:
continue
# Store the max and min feedrates and thicknesses for display purposes
for p in layer_data.getLayer(layer_id).polygons:
self._max_feedrate = max(float(p.lineFeedrates.max()), self._max_feedrate)
@ -634,4 +642,3 @@ class _CreateTopLayersJob(Job):
def cancel(self):
self._cancel = True
super().cancel()

View file

@ -61,7 +61,7 @@ Item
Button {
id: collapseButton
anchors.top: parent.top
anchors.topMargin: Math.floor(UM.Theme.getSize("default_margin").height + (UM.Theme.getSize("layerview_row").height - UM.Theme.getSize("default_margin").height) / 2)
anchors.topMargin: Math.round(UM.Theme.getSize("default_margin").height + (UM.Theme.getSize("layerview_row").height - UM.Theme.getSize("default_margin").height) / 2)
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
@ -193,7 +193,7 @@ Item
Item
{
height: Math.floor(UM.Theme.getSize("default_margin").width / 2)
height: Math.round(UM.Theme.getSize("default_margin").width / 2)
width: width
}
@ -231,7 +231,7 @@ Item
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
color: model.color
radius: width / 2
radius: Math.round(width / 2)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
visible: !viewSettings.show_legend & !viewSettings.show_gradient
@ -249,7 +249,7 @@ Item
anchors.verticalCenter: parent.verticalCenter
anchors.left: extrudersModelCheckBox.left;
anchors.right: extrudersModelCheckBox.right;
anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
anchors.leftMargin: UM.Theme.getSize("checkbox").width + Math.round(UM.Theme.getSize("default_margin").width/2)
anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
}
}
@ -316,7 +316,7 @@ Item
anchors.verticalCenter: parent.verticalCenter
anchors.left: legendModelCheckBox.left;
anchors.right: legendModelCheckBox.right;
anchors.leftMargin: UM.Theme.getSize("checkbox").width + UM.Theme.getSize("default_margin").width /2
anchors.leftMargin: UM.Theme.getSize("checkbox").width + Math.round(UM.Theme.getSize("default_margin").width/2)
anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
}
}
@ -461,7 +461,7 @@ Item
visible: viewSettings.show_feedrate_gradient
anchors.left: parent.right
height: parent.width
width: UM.Theme.getSize("layerview_row").height * 1.5
width: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
@ -492,7 +492,7 @@ Item
visible: viewSettings.show_thickness_gradient
anchors.left: parent.right
height: parent.width
width: UM.Theme.getSize("layerview_row").height * 1.5
width: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}

View file

@ -146,7 +146,7 @@ class SliceInfo(Extension):
model_stack = node.callDecoration("getStack")
if model_stack:
model_settings["support_enabled"] = model_stack.getProperty("support_enable", "value")
model_settings["support_extruder_nr"] = int(model_stack.getProperty("support_extruder_nr", "value"))
model_settings["support_extruder_nr"] = int(model_stack.getExtruderPositionValueWithDefault("support_extruder_nr"))
# Mesh modifiers;
model_settings["infill_mesh"] = model_stack.getProperty("infill_mesh", "value")
@ -177,7 +177,7 @@ class SliceInfo(Extension):
# Support settings
print_settings["support_enabled"] = global_container_stack.getProperty("support_enable", "value")
print_settings["support_extruder_nr"] = int(global_container_stack.getProperty("support_extruder_nr", "value"))
print_settings["support_extruder_nr"] = int(global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr"))
# Platform adhesion settings
print_settings["adhesion_type"] = global_container_stack.getProperty("adhesion_type", "value")

View file

@ -62,7 +62,7 @@ class SolidView(View):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
support_extruder_nr = global_container_stack.getProperty("support_extruder_nr", "value")
support_extruder_nr = global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr")
support_angle_stack = Application.getInstance().getExtruderManager().getExtruderStack(support_extruder_nr)
if support_angle_stack is not None and Preferences.getInstance().getValue("view/show_overhang"):
@ -78,22 +78,18 @@ class SolidView(View):
for node in DepthFirstIterator(scene.getRoot()):
if not node.render(renderer):
if node.getMeshData() and node.isVisible():
if node.getMeshData() and node.isVisible() and not node.callDecoration("getLayerData"):
uniforms = {}
shade_factor = 1.0
per_mesh_stack = node.callDecoration("getStack")
# Get color to render this mesh in from ExtrudersModel
extruder_index = 0
extruder_id = node.callDecoration("getActiveExtruder")
if extruder_id:
extruder_index = max(0, self._extruders_model.find("id", extruder_id))
extruder_index = int(node.callDecoration("getActiveExtruderPosition"))
# Use the support extruder instead of the active extruder if this is a support_mesh
if per_mesh_stack:
if per_mesh_stack.getProperty("support_mesh", "value"):
extruder_index = int(global_container_stack.getProperty("support_extruder_nr", "value"))
extruder_index = int(global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr"))
try:
material_color = self._extruders_model.getItem(extruder_index)["color"]

View file

@ -1,39 +1,101 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Tool import Tool
from PyQt5.QtCore import Qt, QUrl
from UM.Application import Application
from UM.Event import Event
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Settings.SettingInstance import SettingInstance
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
import os
import os.path
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.PickingPass import PickingPass
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
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
class SupportEraser(Tool):
def __init__(self):
super().__init__()
self._shortcut_key = Qt.Key_G
self._controller = Application.getInstance().getController()
self._controller = self.getController()
self._selection_pass = None
Application.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
# toolbar will have been disabled. That is why we need to ignore the first press event
# after the selection has been cleared.
Selection.selectionChanged.connect(self._onSelectionChanged)
self._had_selection = False
self._skip_press = False
self._had_selection_timer = QTimer()
self._had_selection_timer.setInterval(0)
self._had_selection_timer.setSingleShot(True)
self._had_selection_timer.timeout.connect(self._selectionChangeDelay)
def event(self, event):
super().event(event)
modifiers = QApplication.keyboardModifiers()
ctrl_is_active = modifiers & Qt.ControlModifier
if event.type == Event.ToolActivateEvent:
if event.type == Event.MousePressEvent and self._controller.getToolsEnabled():
if ctrl_is_active:
self._controller.setActiveTool("TranslateTool")
return
# Load the remover mesh:
self._createEraserMesh()
if self._skip_press:
# The selection was previously cleared, do not add/remove an anti-support mesh but
# use this click for selection and reactivating this tool only.
self._skip_press = False
return
# After we load the mesh, deactivate the tool again:
self.getController().setActiveTool(None)
if self._selection_pass is None:
# The selection renderpass is used to identify objects in the current view
self._selection_pass = Application.getInstance().getRenderer().getRenderPass("selection")
picked_node = self._controller.getScene().findObject(self._selection_pass.getIdAtPosition(event.x, event.y))
if not picked_node:
# There is no slicable object at the picked location
return
def _createEraserMesh(self):
node_stack = picked_node.callDecoration("getStack")
if node_stack:
if node_stack.getProperty("anti_overhang_mesh", "value"):
self._removeEraserMesh(picked_node)
return
elif node_stack.getProperty("support_mesh", "value") or node_stack.getProperty("infill_mesh", "value") or node_stack.getProperty("cutting_mesh", "value"):
# Only "normal" meshes can have anti_overhang_meshes added to them
return
# Create a pass for picking a world-space location from the mouse location
active_camera = self._controller.getScene().getActiveCamera()
picking_pass = PickingPass(active_camera.getViewportWidth(), active_camera.getViewportHeight())
picking_pass.render()
picked_position = picking_pass.getPickedPosition(event.x, event.y)
# Add the anti_overhang_mesh cube at the picked location
self._createEraserMesh(picked_node, picked_position)
def _createEraserMesh(self, parent: CuraSceneNode, position: Vector):
node = CuraSceneNode()
node.setName("Eraser")
@ -42,27 +104,60 @@ class SupportEraser(Tool):
mesh.addCube(10,10,10)
node.setMeshData(mesh.build())
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
node.addDecorator(SettingOverrideDecorator())
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
node.addDecorator(BuildPlateDecorator(active_build_plate))
node.addDecorator(SliceableObjectDecorator())
stack = node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
if not stack:
node.addDecorator(SettingOverrideDecorator())
stack = node.callDecoration("getStack")
stack = node.callDecoration("getStack") # created by SettingOverrideDecorator that is automatically added to CuraSceneNode
settings = stack.getTop()
if not (settings.getInstance("anti_overhang_mesh") and settings.getProperty("anti_overhang_mesh", "value")):
definition = stack.getSettingDefinition("anti_overhang_mesh")
new_instance = SettingInstance(definition, settings)
new_instance.setProperty("value", True)
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
definition = stack.getSettingDefinition("anti_overhang_mesh")
new_instance = SettingInstance(definition, settings)
new_instance.setProperty("value", True)
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
scene = self._controller.getScene()
op = AddSceneNodeOperation(node, scene.getRoot())
op = AddSceneNodeOperation(node, parent)
op.push()
node.setPosition(position, CuraSceneNode.TransformSpace.World)
Application.getInstance().getController().getScene().sceneChanged.emit(node)
def _removeEraserMesh(self, node: CuraSceneNode):
parent = node.getParent()
if parent == self._controller.getScene().getRoot():
parent = None
op = RemoveSceneNodeOperation(node)
op.push()
if parent and not Selection.isSelected(parent):
Selection.add(parent)
Application.getInstance().getController().getScene().sceneChanged.emit(node)
def _updateEnabled(self):
plugin_enabled = False
global_container_stack = Application.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)
def _onSelectionChanged(self):
# When selection is passed from one object to another object, first the selection is cleared
# and then it is set to the new object. We are only interested in the change from no selection
# to a selection or vice-versa, not in a change from one object to another. A timer is used to
# "merge" a possible clear/select action in a single frame
if Selection.hasSelection() != self._had_selection:
self._had_selection_timer.start()
def _selectionChangeDelay(self):
has_selection = Selection.hasSelection()
if not has_selection and self._had_selection:
self._skip_press = True
else:
self._skip_press = False
self._had_selection = has_selection

View file

@ -23,7 +23,7 @@ class UFPWriter(MeshWriter):
def _createSnapshot(self, *args):
# must be called from the main thread because of OpenGL
Logger.log("d", "Creating thumbnail image...")
self._snapshot = Snapshot.snapshot()
self._snapshot = Snapshot.snapshot(width = 300, height = 300)
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
archive = VirtualFile()

View file

@ -1,13 +1,23 @@
#Copyright (c) 2018 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
from . import UFPWriter
import sys
from UM.Logger import Logger
try:
from . import UFPWriter
except ImportError:
Logger.log("w", "Could not import UFPWriter; libCharon may be missing")
from UM.i18n import i18nCatalog #To translate the file format description.
from UM.Mesh.MeshWriter import MeshWriter #For the binary mode flag.
i18n_catalog = i18nCatalog("cura")
def getMetaData():
if "UFPWriter.UFPWriter" not in sys.modules:
return {}
return {
"mesh_writer": {
"output": [
@ -22,4 +32,7 @@ def getMetaData():
}
def register(app):
if "UFPWriter.UFPWriter" not in sys.modules:
return {}
return { "mesh_writer": UFPWriter.UFPWriter() }

View file

@ -1,12 +1,17 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
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
from UM.Qt.Duration import Duration, DurationFormat
from UM.OutputDevice import OutputDeviceError #To show that something went wrong when writing.
from UM.Scene.SceneNode import SceneNode #For typing.
from UM.Version import Version #To check against firmware versions for support.
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
@ -20,10 +25,11 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
from time import time
from time import time, sleep
from datetime import datetime
from typing import Optional
from typing import Optional, Dict, List
import io #To create the correct buffers for sending data to the printer.
import json
import os
@ -77,26 +83,49 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._cluster_size = int(properties.get(b"cluster_size", 0))
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
self._latest_reply_handler = None
def requestWrite(self, nodes: List[SceneNode], file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
self.writeStarted.emit(self)
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", [])
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
# Unable to find g-code. Nothing to send
return
self._gcode = gcode_list
if len(self._printers) > 1:
self._spawnPrinterSelectionDialog()
#Formats supported by this application (file types that we can actually write).
if file_handler:
file_formats = file_handler.getSupportedFileTypesWrite()
else:
self.sendPrintJob()
file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
# Notify the UI that a switch to the print monitor should happen
Application.getInstance().getController().setActiveStage("MonitorStage")
#Create a list from the supported file formats string.
machine_file_formats = Application.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"):
machine_file_formats = ["application/x-ufp"] + machine_file_formats
# Take the intersection between file_formats and machine_file_formats.
format_by_mimetype = {format["mime_type"]: format for format in file_formats}
file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats.
if len(file_formats) == 0:
Logger.log("e", "There are no file formats available to write with!")
raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!"))
preferred_format = file_formats[0]
#Just take the first file format available.
if file_handler is not None:
writer = file_handler.getWriterByMimeType(preferred_format["mime_type"])
else:
writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(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)
self._sending_job.send(None) #Start the generator.
if len(self._printers) > 1: #We need to ask the user.
self._spawnPrinterSelectionDialog()
is_job_sent = True
else: #Just immediately continue.
self._sending_job.send("") #No specifically selected printer.
is_job_sent = self._sending_job.send(None)
def _spawnPrinterSelectionDialog(self):
if self._printer_selection_dialog is None:
@ -109,29 +138,54 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
def clusterSize(self):
return self._cluster_size
@pyqtSlot()
## Allows the user to choose a printer to print with from the printer
# selection dialogue.
# \param target_printer The name of the printer to target.
@pyqtSlot(str)
def sendPrintJob(self, target_printer = ""):
def selectPrinter(self, target_printer: str = "") -> None:
self._sending_job.send(target_printer)
## Greenlet to send a job to the printer over the network.
#
# This greenlet gets called asynchronously in requestWrite. It is a
# greenlet in order to optionally wait for selectPrinter() to select a
# printer.
# The greenlet yields exactly three times: First time None,
# \param writer The file writer to use to create the data.
# \param preferred_format A dictionary containing some information about
# what format to write to. This is necessary to create the correct buffer
# types and file extension and such.
def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict, nodes: List[SceneNode]):
Logger.log("i", "Sending print job to printer.")
if self._sending_gcode:
self._error_message = Message(
i18n_catalog.i18nc("@info:status",
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
self._error_message.show()
return
yield #Wait on the user to select a target printer.
yield #Wait for the write job to be finished.
yield False #Return whether this was a success or not.
yield #Prevent StopIteration.
self._sending_gcode = True
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
i18n_catalog.i18nc("@info:title", "Sending Data"))
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
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
if preferred_format["mode"] == FileWriter.OutputMode.TextMode:
stream = io.StringIO()
else: #Binary mode.
stream = io.BytesIO()
job = WriteFileJob(writer, stream, nodes, preferred_format["mode"])
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
title = i18n_catalog.i18nc("@info:title", "Sending Data"))
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = None, description = "")
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
self._progress_message.show()
compressed_gcode = self._compressGCode()
if compressed_gcode is None:
# Abort was called.
return
job.start()
parts = []
@ -143,18 +197,27 @@ 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 = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName
file_name = Application.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"]
parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, compressed_gcode))
while not job.isFinished():
sleep(0.1)
output = stream.getvalue() #Either str or bytes depending on the output mode.
if isinstance(stream, io.StringIO):
output = output.encode("utf-8")
self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)
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)
yield True #Return that we had success!
yield #To prevent having to catch the StopIteration exception.
@pyqtProperty(QObject, notify=activePrinterChanged)
def activePrinter(self) -> Optional["PrinterOutputModel"]:
def activePrinter(self) -> Optional[PrinterOutputModel]:
return self._active_printer
@pyqtSlot(QObject)
def setActivePrinter(self, printer):
def setActivePrinter(self, printer: Optional[PrinterOutputModel]):
if self._active_printer != printer:
if self._active_printer and self._active_printer.camera:
self._active_printer.camera.stop()
@ -166,7 +229,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._compressing_gcode = False
self._sending_gcode = False
def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
def _onUploadPrintJobProgress(self, bytes_sent:int, bytes_total:int):
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
@ -175,11 +238,24 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
if new_progress > self._progress_message.getProgress():
self._progress_message.show() # Ensure that the message is visible.
self._progress_message.setProgress(bytes_sent / bytes_total * 100)
# If successfully sent:
if bytes_sent == bytes_total:
# Show a confirmation to the user so they know the job was sucessful and provide the option to switch to the
# monitor tab.
self._success_message = Message(
i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."),
lifetime=5, dismissable=True,
title=i18n_catalog.i18nc("@info:title", "Data Sent"))
self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Montior"), icon=None,
description="")
self._success_message.actionTriggered.connect(self._successMessageActionTriggered)
self._success_message.show()
else:
self._progress_message.setProgress(0)
self._progress_message.hide()
def _progressMessageActionTriggered(self, message_id=None, action_id=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()
@ -187,30 +263,40 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._sending_gcode = False
Application.getInstance().getController().setActiveStage("PrepareStage")
# After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request
# the "reply" should be disconnected
if self._latest_reply_handler:
self._latest_reply_handler.disconnect()
self._latest_reply_handler = None
def _successMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None:
if action_id == "View":
Application.getInstance().getController().setActiveStage("MonitorStage")
@pyqtSlot()
def openPrintJobControlPanel(self):
def openPrintJobControlPanel(self) -> None:
Logger.log("d", "Opening print job control panel...")
QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
@pyqtSlot()
def openPrinterControlPanel(self):
def openPrinterControlPanel(self) -> None:
Logger.log("d", "Opening printer control panel...")
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
@pyqtProperty("QVariantList", notify=printJobsChanged)
def printJobs(self):
def printJobs(self)-> List[PrintJobOutputModel] :
return self._print_jobs
@pyqtProperty("QVariantList", notify=printJobsChanged)
def queuedPrintJobs(self):
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is None]
def queuedPrintJobs(self) -> List[PrintJobOutputModel]:
return [print_job for print_job in self._print_jobs if print_job.state == "queued"]
@pyqtProperty("QVariantList", notify=printJobsChanged)
def activePrintJobs(self):
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None]
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):
def connectedPrintersTypeCount(self) -> List[PrinterOutputModel]:
printer_count = {}
for printer in self._printers:
if printer.type in printer_count:
@ -223,22 +309,22 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
return result
@pyqtSlot(int, result=str)
def formatDuration(self, seconds):
def formatDuration(self, seconds: int) -> str:
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
@pyqtSlot(int, result=str)
def getTimeCompleted(self, time_remaining):
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)
def getDateCompleted(self, time_remaining):
def getDateCompleted(self, time_remaining: int) -> str:
current_time = time()
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
def _printJobStateChanged(self):
def _printJobStateChanged(self) -> None:
username = self._getUserName()
if username is None:
@ -261,13 +347,13 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# Keep a list of all completed jobs so we know if something changed next time.
self._finished_jobs = finished_jobs
def _update(self):
def _update(self) -> None:
if not super()._update():
return
self.get("printers/", onFinished=self._onGetPrintersDataFinished)
self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)
def _onGetPrintJobsFinished(self, reply: QNetworkReply):
def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
if not checkValidGetReply(reply):
return
@ -287,8 +373,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._updatePrintJob(print_job, print_job_data)
if print_job.state != "queued": # Print job should be assigned to a printer.
if print_job.state == "failed":
# Print job was failed, so don't attach it to a printer.
if print_job.state in ["failed", "finished", "aborted"]:
# Print job was already completed, so don't attach it to a printer.
printer = None
else:
printer = self._getPrinterByKey(print_job_data["printer_uuid"])
@ -309,7 +395,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
if job_list_changed:
self.printJobsChanged.emit() # Do a single emit for all print job changes.
def _onGetPrintersDataFinished(self, reply: QNetworkReply):
def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None:
if not checkValidGetReply(reply):
return
@ -338,34 +424,45 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
if removed_printers or printer_list_changed:
self.printersChanged.emit()
def _createPrinterModel(self, data):
def _createPrinterModel(self, data: Dict) -> 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):
def _createPrintJobModel(self, data: Dict) -> 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, data):
def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict) -> 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, data):
def _updatePrinter(self, printer: PrinterOutputModel, data: Dict) -> 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"]
definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"])
if not definitions:
Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"])
return
machine_definition = definitions[0]
printer.updateName(data["friendly_name"])
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
if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False):
printer.updateBuildplateName(data["build_plate"]["type"])
if not data["enabled"]:
printer.updateState("disabled")
else:
@ -396,13 +493,13 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
color = material_data["color"]
brand = material_data["brand"]
material_type = material_data["material"]
name = "Unknown"
name = "Empty" if material_data["material"] == "empty" else "Unknown"
material = MaterialOutputModel(guid=material_data["guid"], type=material_type,
brand=brand, color=color, name=name)
extruder.updateActiveMaterial(material)
def _removeJob(self, job):
def _removeJob(self, job: PrintJobOutputModel):
if job not in self._print_jobs:
return False
@ -413,7 +510,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
return True
def _removePrinter(self, printer):
def _removePrinter(self, printer: PrinterOutputModel):
self._printers.remove(printer)
if self._active_printer == printer:
self._active_printer = None

View file

@ -13,7 +13,9 @@ class ClusterUM3PrinterOutputController(PrinterOutputController):
def __init__(self, output_device):
super().__init__(output_device)
self.can_pre_heat_bed = False
self.can_pre_heat_hotends = False
self.can_control_manually = False
self.can_send_raw_gcode = False
def setJobState(self, job: "PrintJobOutputModel", state: str):
data = "{\"action\": \"%s\"}" % state

View file

@ -97,6 +97,25 @@ class DiscoverUM3Action(MachineAction):
else:
return []
@pyqtSlot(str)
def setGroupName(self, group_name):
Logger.log("d", "Attempting to set the group name of the active machine to %s", group_name)
global_container_stack = Application.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)
else:
global_container_stack.addMetaDataEntry("connect_group_name", group_name)
global_container_stack.addMetaDataEntry("hidden", False)
if self._network_plugin:
# Ensure that the connection states are refreshed.
self._network_plugin.reCheckConnections()
@pyqtSlot(str)
def setKey(self, key):
Logger.log("d", "Attempting to set the network key of the active machine to %s", key)
@ -104,11 +123,13 @@ class DiscoverUM3Action(MachineAction):
if global_container_stack:
meta_data = global_container_stack.getMetaData()
if "um_network_key" in meta_data:
previous_network_key= meta_data["um_network_key"]
global_container_stack.setMetaDataEntry("um_network_key", key)
# Delete old authentication data.
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)
else:
global_container_stack.addMetaDataEntry("um_network_key", key)
@ -126,6 +147,10 @@ class DiscoverUM3Action(MachineAction):
return ""
@pyqtSlot(str, result = bool)
def existsKey(self, key) -> bool:
return Application.getInstance().getMachineManager().existNetworkInstances(network_key = key)
@pyqtSlot()
def loadConfigurationFromPrinter(self):
machine_manager = Application.getInstance().getMachineManager()

View file

@ -5,6 +5,7 @@ import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import QtQuick.Dialogs 1.2
Cura.MachineAction
{
@ -32,14 +33,34 @@ Cura.MachineAction
if(base.selectedDevice && base.completeProperties)
{
var printerKey = base.selectedDevice.key
if(manager.getStoredKey() != printerKey)
var printerName = base.selectedDevice.name // TODO To change when the groups have a name
if (manager.getStoredKey() != printerKey)
{
manager.setKey(printerKey);
completed();
// Check if there is another instance with the same key
if (!manager.existsKey(printerKey))
{
manager.setKey(printerKey)
manager.setGroupName(printerName) // TODO To change when the groups have a name
completed()
}
else
{
existingConnectionDialog.open()
}
}
}
}
MessageDialog
{
id: existingConnectionDialog
title: catalog.i18nc("@window:title", "Existing Connection")
icon: StandardIcon.Information
text: catalog.i18nc("@message:text", "This printer/group is already added to Cura. Please select another printer/group.")
standardButtons: StandardButton.Ok
modality: Qt.ApplicationModal
}
Column
{
anchors.fill: parent;
@ -114,7 +135,7 @@ Cura.MachineAction
Column
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
spacing: UM.Theme.getSize("default_margin").height
ScrollView
@ -198,7 +219,7 @@ Cura.MachineAction
}
Column
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
visible: base.selectedDevice ? true : false
spacing: UM.Theme.getSize("default_margin").height
Label
@ -216,13 +237,13 @@ Cura.MachineAction
columns: 2
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Type")
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text:
{
@ -247,25 +268,25 @@ Cura.MachineAction
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Firmware version")
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: base.selectedDevice ? base.selectedDevice.firmwareVersion : ""
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Address")
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: base.selectedDevice ? base.selectedDevice.ipAddress : ""
}
@ -303,7 +324,7 @@ Cura.MachineAction
Button
{
text: catalog.i18nc("@action:button", "Connect")
enabled: (base.selectedDevice && base.completeProperties) ? true : false
enabled: (base.selectedDevice && base.completeProperties && base.selectedDevice.clusterSize > 0) ? true : false
onClicked: connectToPrinter()
}
}

View file

@ -184,7 +184,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
self.writeStarted.emit(self)
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", [])
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
@ -419,8 +419,6 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
self._authentication_failed_message.show()
elif status_code == 200:
self.setAuthenticationState(AuthState.Authenticated)
# Now we know for sure that we are authenticated, send the material profiles to the machine.
self._sendMaterialProfiles()
def _checkAuthentication(self):
Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())

View file

@ -20,6 +20,7 @@ class LegacyUM3PrinterOutputController(PrinterOutputController):
self._preheat_printer = None
self.can_control_manually = False
self.can_send_raw_gcode = False
# Are we still waiting for a response about preheat?
# We need this so we can already update buttons, so it feels more snappy.

View file

@ -10,7 +10,7 @@ Item
id: extruderInfo
property var printCoreConfiguration
width: Math.floor(parent.width / 2)
width: Math.round(parent.width / 2)
height: childrenRect.height
Label
{

View file

@ -101,7 +101,7 @@ UM.Dialog
enabled: true
onClicked: {
base.visible = false;
OutputDevice.sendPrintJob(printerSelectionCombobox.model.get(printerSelectionCombobox.currentIndex).key)
OutputDevice.selectPrinter(printerSelectionCombobox.model.get(printerSelectionCombobox.currentIndex).key)
// reset to defaults
printerSelectionCombobox.currentIndex = 0
}

View file

@ -34,17 +34,17 @@ Rectangle
switch (printer.state)
{
case "pre_print":
return catalog.i18nc("@label", "Preparing to print")
return catalog.i18nc("@label:status", "Preparing to print")
case "printing":
return catalog.i18nc("@label:status", "Printing");
case "idle":
return catalog.i18nc("@label:status", "Available");
case "unreachable":
return catalog.i18nc("@label:MonitorStatus", "Lost connection with the printer");
case "maintenance": // TODO: new string
case "unknown":
return catalog.i18nc("@label:status", "Lost connection with the printer");
case "maintenance":
return catalog.i18nc("@label:status", "Unavailable");
default:
return catalog.i18nc("@label Printer status", "Unknown");
return catalog.i18nc("@label:status", "Unknown");
}
}
@ -78,7 +78,7 @@ Rectangle
Rectangle
{
width: Math.floor(parent.width / 3)
width: Math.round(parent.width / 3)
height: parent.height
Label // Print job name
@ -123,7 +123,7 @@ Rectangle
Rectangle
{
width: Math.floor(parent.width / 3 * 2)
width: Math.round(parent.width / 3 * 2)
height: parent.height
Label // Friendly machine name
@ -131,7 +131,7 @@ Rectangle
id: printerNameLabel
anchors.top: parent.top
anchors.left: parent.left
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width)
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width)
text: printer.name
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
@ -141,7 +141,7 @@ Rectangle
{
id: printerTypeLabel
anchors.top: printerNameLabel.bottom
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
text: printer.type
anchors.left: parent.left
elide: Text.ElideRight
@ -175,7 +175,7 @@ Rectangle
id: extruderInfo
anchors.bottom: parent.bottom
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
@ -183,7 +183,7 @@ Rectangle
PrintCoreConfiguration
{
id: leftExtruderInfo
width: Math.floor((parent.width - extruderSeperator.width) / 2)
width: Math.round((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.extruders[0]
}
@ -198,7 +198,7 @@ Rectangle
PrintCoreConfiguration
{
id: rightExtruderInfo
width: Math.floor((parent.width - extruderSeperator.width) / 2)
width: Math.round((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.extruders[1]
}
}
@ -209,7 +209,7 @@ Rectangle
anchors.right: parent.right
anchors.top: parent.top
height: showExtended ? parent.height: printProgressTitleBar.height
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
border.width: UM.Theme.getSize("default_lining").width
border.color: lineColor
radius: cornerRadius
@ -264,6 +264,7 @@ Rectangle
case "wait_for_configuration":
return catalog.i18nc("@label:status", "Reserved")
case "wait_cleanup":
case "wait_user_action":
return catalog.i18nc("@label:status", "Finished")
case "pre_print":
case "sent_to_printer":
@ -278,6 +279,7 @@ Rectangle
case "aborted":
return catalog.i18nc("@label:status", "Print aborted");
default:
// If print job has unknown status show printer.status
return printerStatusText(printer);
}
}

View file

@ -57,7 +57,7 @@ Item
{
id: cameraImage
width: Math.min(sourceSize.width === 0 ? 800 * screenScaleFactor : sourceSize.width, maximumWidth)
height: Math.floor((sourceSize.height === 0 ? 600 * screenScaleFactor : sourceSize.height) * width / sourceSize.width)
height: Math.round((sourceSize.height === 0 ? 600 * screenScaleFactor : sourceSize.height) * width / sourceSize.width)
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
z: 1

View file

@ -10,7 +10,8 @@ Item
{
id: base
property bool isUM3: Cura.MachineManager.activeQualityDefinitionId == "ultimaker3"
property string activeQualityDefinitionId: Cura.MachineManager.activeQualityDefinitionId
property bool isUM3: activeQualityDefinitionId == "ultimaker3" || activeQualityDefinitionId.match("ultimaker_") != null
property bool printerConnected: Cura.MachineManager.printerOutputDevices.length != 0
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
property bool authenticationRequested: printerConnected && (Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 || Cura.MachineManager.printerOutputDevices[0].authenticationState == 5) // AuthState.AuthenticationRequested or AuthenticationReceived.

View file

@ -82,6 +82,9 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
self._zero_conf_browser.cancel()
self._zero_conf_browser = None # Force the old ServiceBrowser to be destroyed.
for instance_name in list(self._discovered_devices):
self._onRemoveDevice(instance_name)
self._zero_conf = Zeroconf()
self._zero_conf_browser = ServiceBrowser(self._zero_conf, u'_ultimaker._tcp.local.',
[self._appendServiceChangedRequest])

View file

@ -22,6 +22,7 @@ class AutoDetectBaudJob(Job):
def run(self):
Logger.log("d", "Auto detect baud rate started.")
timeout = 3
tries = 2
programmer = Stk500v2()
serial = None
@ -31,36 +32,38 @@ class AutoDetectBaudJob(Job):
except:
programmer.close()
for baud_rate in self._all_baud_rates:
Logger.log("d", "Checking {serial} if baud rate {baud_rate} works".format(serial= self._serial_port, baud_rate = baud_rate))
for retry in range(tries):
for baud_rate in self._all_baud_rates:
Logger.log("d", "Checking {serial} if baud rate {baud_rate} works".format(serial= self._serial_port, baud_rate = baud_rate))
if serial is None:
try:
serial = Serial(str(self._serial_port), baud_rate, timeout = timeout, writeTimeout = timeout)
except SerialException as e:
Logger.logException("w", "Unable to create serial")
continue
else:
# We already have a serial connection, just change the baud rate.
try:
serial.baudrate = baud_rate
except:
continue
sleep(1.5) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number
successful_responses = 0
serial.write(b"\n") # Ensure we clear out previous responses
serial.write(b"M105\n")
timeout_time = time() + timeout
while timeout_time > time():
line = serial.readline()
if b"ok T:" in line:
successful_responses += 1
if successful_responses >= 3:
self.setResult(baud_rate)
return
if serial is None:
try:
serial = Serial(str(self._serial_port), baud_rate, timeout = timeout, writeTimeout = timeout)
except SerialException as e:
Logger.logException("w", "Unable to create serial")
continue
else:
# We already have a serial connection, just change the baud rate.
try:
serial.baudrate = baud_rate
except:
continue
sleep(1.5) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number
successful_responses = 0
serial.write(b"\n") # Ensure we clear out previous responses
serial.write(b"M105\n")
timeout_time = time() + timeout
while timeout_time > time():
line = serial.readline()
if b"ok T:" in line:
successful_responses += 1
if successful_responses >= 3:
self.setResult(baud_rate)
return
serial.write(b"M105\n")
sleep(15) # Give the printer some time to init and try again.
self.setResult(None) # Unable to detect the correct baudrate.

View file

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

View file

@ -1,4 +1,4 @@
# Copyright (c) 2016 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
@ -10,14 +10,14 @@ from UM.PluginRegistry import PluginRegistry
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.GenericOutputController import GenericOutputController
from .AutoDetectBaudJob import AutoDetectBaudJob
from .USBPrinterOutputController import USBPrinterOutputController
from .avr_isp import stk500v2, intelHex
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
from serial import Serial, SerialException
from serial import Serial, SerialException, SerialTimeoutException
from threading import Thread
from time import time, sleep
from queue import Queue
@ -99,7 +99,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
Application.getInstance().getController().setActiveStage("MonitorStage")
# find the G-code for the active build plate to print
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict")
gcode_list = gcode_dict[active_build_plate_id]
@ -116,7 +116,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
@pyqtSlot(str)
def updateFirmware(self, file):
self._firmware_location = file
# the file path is qurl encoded.
self._firmware_location = file.replace("file://", "")
self.showFirmwareInterface()
self.setFirmwareUpdateState(FirmwareUpdateState.updating)
self._update_firmware_thread.start()
@ -126,9 +127,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if self._connection_state != ConnectionState.closed:
self.close()
hex_file = intelHex.readHex(self._firmware_location)
if len(hex_file) == 0:
Logger.log("e", "Unable to read provided hex file. Could not update firmware")
try:
hex_file = intelHex.readHex(self._firmware_location)
assert len(hex_file) > 0
except (FileNotFoundError, AssertionError):
Logger.log("e", "Unable to read provided hex file. Could not update firmware.")
self.setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error)
return
@ -198,7 +201,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# Reset line number. If this is not done, first line is sometimes ignored
self._gcode.insert(0, "M110")
self._gcode_position = 0
self._is_printing = True
self._print_start_time = time()
self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))
@ -206,6 +208,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
for i in range(0, 4): # Push first 4 entries before accepting other inputs
self._sendNextGcodeLine()
self._is_printing = True
self.writeFinished.emit(self)
def _autoDetectFinished(self, job: AutoDetectBaudJob):
@ -237,7 +240,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
container_stack = Application.getInstance().getGlobalContainerStack()
num_extruders = container_stack.getProperty("machine_extruder_count", "value")
# Ensure that a printer is created.
self._printers = [PrinterOutputModel(output_controller=USBPrinterOutputController(self), number_of_extruders=num_extruders)]
self._printers = [PrinterOutputModel(output_controller=GenericOutputController(self), number_of_extruders=num_extruders)]
self._printers[0].updateName(container_stack.getName())
self.setConnectionState(ConnectionState.connected)
self._update_thread.start()
@ -266,8 +269,10 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
command = (command + "\n").encode()
if not command.endswith(b"\n"):
command += b"\n"
self._serial.write(b"\n")
self._serial.write(command)
try:
self._serial.write(command)
except SerialTimeoutException:
Logger.log("w", "Timeout when sending command to printer via USB.")
def _update(self):
while self._connection_state == ConnectionState.connected and self._serial is not None:
@ -281,7 +286,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self.sendCommand("M105")
self._last_temperature_request = time()
if b"ok T:" in line or line.startswith(b"T:"): # Temperature message
if b"ok T:" in line or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed
extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
# Update all temperature values
for match, extruder in zip(extruder_temperature_matches, self._printers[0].extruders):
@ -299,6 +304,9 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._printers[0].updateTargetBedTemperature(float(match[1]))
if self._is_printing:
if line.startswith(b'!!'):
Logger.log('e', "Printer signals fatal error. Cancelling print. {}".format(line))
self.cancelPrint()
if b"ok" in line:
if not self._command_queue.empty():
self._sendCommand(self._command_queue.get())
@ -364,7 +372,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
elapsed_time = int(time() - self._print_start_time)
print_job = self._printers[0].activePrintJob
if print_job is None:
print_job = PrintJobOutputModel(output_controller = USBPrinterOutputController(self), name= Application.getInstance().getPrintInformation().jobName)
print_job = PrintJobOutputModel(output_controller = GenericOutputController(self), name= Application.getInstance().getPrintInformation().jobName)
print_job.updateState("printing")
self._printers[0].updateActivePrintJob(print_job)

View file

@ -1,3 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import List
from cura.MachineAction import MachineAction
from cura.PrinterOutputDevice import PrinterOutputDevice
@ -5,6 +10,7 @@ from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
from UM.i18n import i18nCatalog
from UM.Logger import Logger
catalog = i18nCatalog("cura")
@ -26,38 +32,45 @@ class BedLevelMachineAction(MachineAction):
@pyqtSlot()
def startBedLeveling(self):
self._bed_level_position = 0
printer_output_devices = self._getPrinterOutputDevices()
if printer_output_devices:
printer_output_devices[0].homeBed()
printer_output_devices[0].moveHead(0, 0, 3)
printer_output_devices[0].homeHead()
def _getPrinterOutputDevices(self):
printer_output_devices = self._getPrinterOutputDevices()
if not printer_output_devices:
Logger.log("e", "Can't start bed levelling. The printer connection seems to have been lost.")
return
printer = printer_output_devices[0].activePrinter
printer.homeBed()
printer.moveHead(0, 0, 3)
printer.homeHead()
def _getPrinterOutputDevices(self) -> List[PrinterOutputDevice]:
return [printer_output_device for printer_output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices() if isinstance(printer_output_device, PrinterOutputDevice)]
@pyqtSlot()
def moveToNextLevelPosition(self):
output_devices = self._getPrinterOutputDevices()
if output_devices: # We found at least one output device
output_device = output_devices[0]
if not output_devices: #No output devices. Can't move.
Logger.log("e", "Can't move to the next position. The printer connection seems to have been lost.")
return
printer = output_devices[0].activePrinter
if self._bed_level_position == 0:
output_device.moveHead(0, 0, 3)
output_device.homeHead()
output_device.moveHead(0, 0, 3)
output_device.moveHead(Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value") - 10, 0, 0)
output_device.moveHead(0, 0, -3)
self._bed_level_position += 1
elif self._bed_level_position == 1:
output_device.moveHead(0, 0, 3)
output_device.moveHead(-Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value" ) / 2, Application.getInstance().getGlobalContainerStack().getProperty("machine_depth", "value") - 10, 0)
output_device.moveHead(0, 0, -3)
self._bed_level_position += 1
elif self._bed_level_position == 2:
output_device.moveHead(0, 0, 3)
output_device.moveHead(-Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value") / 2 + 10, -(Application.getInstance().getGlobalContainerStack().getProperty("machine_depth", "value") + 10), 0)
output_device.moveHead(0, 0, -3)
self._bed_level_position += 1
elif self._bed_level_position >= 3:
output_device.sendCommand("M18") # Turn off all motors so the user can move the axes
self.setFinished()
if self._bed_level_position == 0:
printer.moveHead(0, 0, 3)
printer.homeHead()
printer.moveHead(0, 0, 3)
printer.moveHead(Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value") - 10, 0, 0)
printer.moveHead(0, 0, -3)
self._bed_level_position += 1
elif self._bed_level_position == 1:
printer.moveHead(0, 0, 3)
printer.moveHead(-Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value" ) / 2, Application.getInstance().getGlobalContainerStack().getProperty("machine_depth", "value") - 10, 0)
printer.moveHead(0, 0, -3)
self._bed_level_position += 1
elif self._bed_level_position == 2:
printer.moveHead(0, 0, 3)
printer.moveHead(-Application.getInstance().getGlobalContainerStack().getProperty("machine_width", "value") / 2 + 10, -(Application.getInstance().getGlobalContainerStack().getProperty("machine_depth", "value") + 10), 0)
printer.moveHead(0, 0, -3)
self._bed_level_position += 1
elif self._bed_level_position >= 3:
output_devices[0].sendCommand("M18") # Turn off all motors so the user can move the axes
self.setFinished()

View file

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

View file

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

View file

@ -153,6 +153,10 @@ class VersionUpgrade26to27(VersionUpgrade):
if new_id is not None:
parser.set("containers", key, new_id)
if "6" not in parser["containers"]:
parser["containers"]["6"] = parser["containers"]["5"]
parser["containers"]["5"] = "empty"
for each_section in ("general", "metadata"):
if not parser.has_section(each_section):
parser.add_section(each_section)

View file

@ -3,14 +3,9 @@
import configparser #To parse preference files.
import io #To serialise the preference files afterwards.
import os
from urllib.parse import quote_plus
from UM.Resources import Resources
from UM.VersionUpgrade import VersionUpgrade #We're inheriting from this.
from cura.CuraApplication import CuraApplication
# a list of all legacy "Not Supported" quality profiles
_OLD_NOT_SUPPORTED_PROFILES = [
@ -130,7 +125,6 @@ class VersionUpgrade30to31(VersionUpgrade):
parser.write(output)
return [filename], [output.getvalue()]
## Upgrades a container stack from version 3.0 to 3.1.
#
# \param serialised The serialised form of a container stack.
@ -172,71 +166,3 @@ class VersionUpgrade30to31(VersionUpgrade):
output = io.StringIO()
parser.write(output)
return [filename], [output.getvalue()]
def _getSingleExtrusionMachineQualityChanges(self, quality_changes_container):
quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
quality_changes_containers = []
for item in os.listdir(quality_changes_dir):
file_path = os.path.join(quality_changes_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation = None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("metadata", "type"):
continue
if "quality_changes" != parser["metadata"]["type"]:
continue
if not parser.has_option("general", "name"):
continue
if quality_changes_container["general"]["name"] != parser["general"]["name"]:
continue
quality_changes_containers.append(parser)
return quality_changes_containers
def _createExtruderQualityChangesForSingleExtrusionMachine(self, filename, global_quality_changes):
suffix = "_" + quote_plus(global_quality_changes["general"]["name"].lower())
machine_name = os.path.os.path.basename(filename).replace(".inst.cfg", "").replace(suffix, "")
# Why is this here?!
# When we load a .curaprofile file the deserialize will trigger a version upgrade, creating a dangling file.
# This file can be recognized by it's lack of a machine name in the target filename.
# So when we detect that situation here, we don't create the file and return.
if machine_name == "":
return
new_filename = machine_name + "_" + "fdmextruder" + suffix
extruder_quality_changes_parser = configparser.ConfigParser(interpolation = None)
extruder_quality_changes_parser.add_section("general")
extruder_quality_changes_parser["general"]["version"] = str(2)
extruder_quality_changes_parser["general"]["name"] = global_quality_changes["general"]["name"]
extruder_quality_changes_parser["general"]["definition"] = global_quality_changes["general"]["definition"]
# check renamed definition
if extruder_quality_changes_parser["general"]["definition"] in _RENAMED_DEFINITION_DICT:
extruder_quality_changes_parser["general"]["definition"] = _RENAMED_DEFINITION_DICT[extruder_quality_changes_parser["general"]["definition"]]
extruder_quality_changes_parser.add_section("metadata")
extruder_quality_changes_parser["metadata"]["quality_type"] = global_quality_changes["metadata"]["quality_type"]
extruder_quality_changes_parser["metadata"]["type"] = global_quality_changes["metadata"]["type"]
extruder_quality_changes_parser["metadata"]["setting_version"] = str(4)
extruder_quality_changes_parser["metadata"]["extruder"] = "fdmextruder"
extruder_quality_changes_output = io.StringIO()
extruder_quality_changes_parser.write(extruder_quality_changes_output)
extruder_quality_changes_filename = quote_plus(new_filename) + ".inst.cfg"
quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
with open(os.path.join(quality_changes_dir, extruder_quality_changes_filename), "w", encoding = "utf-8") as f:
f.write(extruder_quality_changes_output.getvalue())

View file

@ -33,6 +33,10 @@ def getMetaData():
"get_version": upgrade.getCfgVersion,
"location": {"./extruders"}
},
"quality": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality"}
},
"quality_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality"}

View file

@ -0,0 +1,138 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser #To parse preference files.
import io #To serialise the preference files afterwards.
from UM.VersionUpgrade import VersionUpgrade #We're inheriting from this.
## Mapping extruder definition IDs to the positions that they are in.
_EXTRUDER_TO_POSITION = {
"builder_premium_large_front": 1,
"builder_premium_large_rear": 0,
"builder_premium_medium_front": 1,
"builder_premium_medium_rear": 0,
"builder_premium_small_front": 1,
"builder_premium_small_rear": 0,
"cartesio_extruder_0": 0,
"cartesio_extruder_1": 1,
"cartesio_extruder_2": 2,
"cartesio_extruder_3": 3,
"custom_extruder_1": 0, #Warning, non-programmers are attempting to count here.
"custom_extruder_2": 1,
"custom_extruder_3": 2,
"custom_extruder_4": 3,
"custom_extruder_5": 4,
"custom_extruder_6": 5,
"custom_extruder_7": 6,
"custom_extruder_8": 7,
"hBp_extruder_left": 0,
"hBp_extruder_right": 1,
"makeit_dual_1st": 0,
"makeit_dual_2nd": 1,
"makeit_l_dual_1st": 0,
"makeit_l_dual_2nd": 1,
"ord_extruder_0": 0,
"ord_extruder_1": 1,
"ord_extruder_2": 2,
"ord_extruder_3": 3,
"ord_extruder_4": 4,
"punchtec_connect_xl_extruder_left": 0,
"punchtec_connect_xl_extruder_right": 1,
"raise3D_N2_dual_extruder_0": 0,
"raise3D_N2_dual_extruder_1": 1,
"raise3D_N2_plus_dual_extruder_0": 0,
"raise3D_N2_plus_dual_extruder_1": 1,
"ultimaker3_extended_extruder_left": 0,
"ultimaker3_extended_extruder_right": 1,
"ultimaker3_extruder_left": 0,
"ultimaker3_extruder_right": 1,
"ultimaker_original_dual_1st": 0,
"ultimaker_original_dual_2nd": 1,
"vertex_k8400_dual_1st": 0,
"vertex_k8400_dual_2nd": 1
}
## Upgrades configurations from the state they were in at version 3.2 to the
# state they should be in at version 3.3.
class VersionUpgrade32to33(VersionUpgrade):
temporary_group_name_counter = 1
## Gets the version number from a CFG file in Uranium's 3.2 format.
#
# Since the format may change, this is implemented for the 3.2 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 a container stack from version 3.2 to 3.3.
#
# \param serialised The serialised form of a container stack.
# \param filename The name of the file to upgrade.
def upgradeStack(self, serialized, filename):
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
if "metadata" in parser and "um_network_key" in parser["metadata"]:
if "hidden" not in parser["metadata"]:
parser["metadata"]["hidden"] = "False"
if "connect_group_name" not in parser["metadata"]:
parser["metadata"]["connect_group_name"] = "Temporary group name #" + str(self.temporary_group_name_counter)
self.temporary_group_name_counter += 1
#Update version number.
parser["general"]["version"] = "4"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
## Upgrades non-quality-changes 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"] = "3"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
## Upgrades a quality changes container to the new format.
def upgradeQualityChanges(self, serialized, filename):
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
#Extruder quality changes profiles have the extruder position instead of the ID of the extruder definition.
if "metadata" in parser and "extruder" in parser["metadata"]: #Only do this for extruder profiles.
extruder_id = parser["metadata"]["extruder"]
if extruder_id in _EXTRUDER_TO_POSITION:
extruder_position = _EXTRUDER_TO_POSITION[extruder_id]
else:
extruder_position = 0 #The user was using custom extruder definitions. He's on his own then.
parser["metadata"]["position"] = str(extruder_position)
del parser["metadata"]["extruder"]
quality_type = parser["metadata"]["quality_type"]
parser["metadata"]["quality_type"] = quality_type.lower()
#Update version number.
parser["general"]["version"] = "3"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]

View file

@ -0,0 +1,44 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import VersionUpgrade32to33
upgrade = VersionUpgrade32to33.VersionUpgrade32to33()
def getMetaData():
return {
"version_upgrade": {
# From To Upgrade function
("machine_stack", 3000004): ("machine_stack", 4000004, upgrade.upgradeStack),
("extruder_train", 3000004): ("extruder_train", 4000004, upgrade.upgradeStack),
("definition_changes", 2000004): ("definition_changes", 3000004, upgrade.upgradeInstanceContainer),
("quality_changes", 2000004): ("quality_changes", 3000004, upgrade.upgradeQualityChanges),
("user", 2000004): ("user", 3000004, upgrade.upgradeInstanceContainer)
},
"sources": {
"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"}
},
"user": {
"get_version": upgrade.getCfgVersion,
"location": {"./user"}
}
}
}
def register(app):
return { "version_upgrade": upgrade }

View file

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

View file

@ -1,4 +1,4 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
@ -10,7 +10,7 @@ from UM.View.RenderPass import RenderPass
from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL
from UM.Scene.SceneNode import SceneNode
from cura.Scene.CuraSceneNode import CuraSceneNode
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
class XRayPass(RenderPass):
@ -27,7 +27,7 @@ class XRayPass(RenderPass):
batch = RenderBatch(self._shader, type = RenderBatch.RenderType.NoType, backface_cull = False, blend_mode = RenderBatch.BlendMode.Additive)
for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is SceneNode and node.getMeshData() and node.isVisible():
if isinstance(node, CuraSceneNode) and node.getMeshData() and node.isVisible():
batch.addItem(node.getWorldTransformation(), node.getMeshData())
self.bind()

View file

@ -13,9 +13,9 @@ vertex =
}
fragment =
uniform sampler2D u_layer0;
uniform sampler2D u_layer1;
uniform sampler2D u_layer2;
uniform sampler2D u_layer0; //Default pass.
uniform sampler2D u_layer1; //Selection pass.
uniform sampler2D u_layer2; //X-ray pass.
uniform vec2 u_offset[9];
@ -83,9 +83,9 @@ vertex41core =
fragment41core =
#version 410
uniform sampler2D u_layer0;
uniform sampler2D u_layer1;
uniform sampler2D u_layer2;
uniform sampler2D u_layer0; //Default pass.
uniform sampler2D u_layer1; //Selection pass.
uniform sampler2D u_layer2; //X-ray pass.
uniform vec2 u_offset[9];

View file

@ -48,18 +48,35 @@ class XmlMaterialProfile(InstanceContainer):
## Overridden from InstanceContainer
# set the meta data for all machine / variant combinations
def setMetaDataEntry(self, key, value):
#
# The "apply_to_all" flag indicates whether this piece of metadata should be applied to all material containers
# or just this specific container.
# For example, when you change the material name, you want to apply it to all its derived containers, but for
# some specific settings, they should only be applied to a machine/variant-specific container.
#
def setMetaDataEntry(self, key, value, apply_to_all = True):
registry = ContainerRegistry.getInstance()
if registry.isReadOnly(self.getId()):
return
super().setMetaDataEntry(key, value)
# Prevent recursion
if not apply_to_all:
super().setMetaDataEntry(key, value)
return
basefile = self.getMetaDataEntry("base_file", self.getId()) #if basefile is self.getId, this is a basefile.
# Update all containers that share basefile
for container in registry.findInstanceContainers(base_file = basefile):
if container.getMetaDataEntry(key, None) != value: # Prevent recursion
container.setMetaDataEntry(key, value)
# Get the MaterialGroup
material_manager = CuraApplication.getInstance().getMaterialManager()
root_material_id = self.getMetaDataEntry("base_file") #if basefile is self.getId, this is a basefile.
material_group = material_manager.getMaterialGroup(root_material_id)
# Update the root material container
root_material_container = material_group.root_material_node.getContainer()
root_material_container.setMetaDataEntry(key, value, apply_to_all = False)
# Update all containers derived from it
for node in material_group.derived_material_node_list:
container = node.getContainer()
container.setMetaDataEntry(key, value, apply_to_all = False)
## Overridden from InstanceContainer, similar to setMetaDataEntry.
# without this function the setName would only set the name of the specific nozzle / material / machine combination container
@ -183,28 +200,34 @@ class XmlMaterialProfile(InstanceContainer):
## Begin Settings Block
builder.start("settings")
if self.getDefinition().getId() == "fdmprinter":
if self.getMetaDataEntry("definition") == "fdmprinter":
for instance in self.findInstances():
self._addSettingElement(builder, instance)
machine_container_map = {}
machine_nozzle_map = {}
machine_variant_map = {}
variant_manager = CuraApplication.getInstance().getVariantManager()
root_material_id = self.getMetaDataEntry("base_file") # if basefile is self.getId, this is a basefile.
all_containers = registry.findInstanceContainers(base_file = root_material_id)
all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"), base_file = self.getId())
for container in all_containers:
definition_id = container.getDefinition().getId()
definition_id = container.getMetaDataEntry("definition")
if definition_id == "fdmprinter":
continue
if definition_id not in machine_container_map:
machine_container_map[definition_id] = container
if definition_id not in machine_nozzle_map:
machine_nozzle_map[definition_id] = {}
if definition_id not in machine_variant_map:
machine_variant_map[definition_id] = {}
variant = container.getMetaDataEntry("variant")
if variant:
machine_nozzle_map[definition_id][variant] = container
variant_name = container.getMetaDataEntry("variant_name")
if variant_name:
variant_dict = {"variant_node": variant_manager.getVariantNode(definition_id, variant_name),
"material_container": container}
machine_variant_map[definition_id][variant_name] = variant_dict
continue
machine_container_map[definition_id] = container
@ -213,7 +236,8 @@ class XmlMaterialProfile(InstanceContainer):
product_id_map = self.getProductIdMap()
for definition_id, container in machine_container_map.items():
definition = container.getDefinition()
definition_id = container.getMetaDataEntry("definition")
definition_metadata = registry.findDefinitionContainersMetadata(id = definition_id)[0]
product = definition_id
for product_name, product_id_list in product_id_map.items():
@ -223,45 +247,74 @@ class XmlMaterialProfile(InstanceContainer):
builder.start("machine")
builder.start("machine_identifier", {
"manufacturer": container.getMetaDataEntry("machine_manufacturer", definition.getMetaDataEntry("manufacturer", "Unknown")),
"manufacturer": container.getMetaDataEntry("machine_manufacturer",
definition_metadata.get("manufacturer", "Unknown")),
"product": product
})
builder.end("machine_identifier")
for instance in container.findInstances():
if self.getDefinition().getId() == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value:
if self.getMetaDataEntry("definition") == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value:
# If the settings match that of the base profile, just skip since we inherit the base profile.
continue
self._addSettingElement(builder, instance)
# Find all hotend sub-profiles corresponding to this material and machine and add them to this profile.
for hotend_id, hotend in machine_nozzle_map[definition_id].items():
variant_containers = registry.findInstanceContainersMetadata(id = hotend.getMetaDataEntry("variant"))
if not variant_containers:
continue
buildplate_dict = {}
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
variant_type = VariantType(variant_type)
if variant_type == VariantType.NOZZLE:
# The hotend identifier is not the containers name, but its "name".
builder.start("hotend", {"id": variant_name})
# The hotend identifier is not the containers name, but its "name".
builder.start("hotend", {"id": variant_containers[0]["name"]})
# Compatible is a special case, as it's added as a meta data entry (instead of an instance).
material_container = variant_dict["material_container"]
compatible = material_container.getMetaDataEntry("compatible")
if compatible is not None:
builder.start("setting", {"key": "hardware compatible"})
if compatible:
builder.data("yes")
else:
builder.data("no")
builder.end("setting")
# Compatible is a special case, as it's added as a meta data entry (instead of an instance).
compatible = hotend.getMetaDataEntry("compatible")
if compatible is not None:
builder.start("setting", {"key": "hardware compatible"})
if compatible:
builder.data("yes")
else:
builder.data("no")
builder.end("setting")
for instance in material_container.findInstances():
if container.getInstance(instance.definition.key) and container.getProperty(instance.definition.key, "value") == instance.value:
# If the settings match that of the machine profile, just skip since we inherit the machine profile.
continue
for instance in hotend.findInstances():
if container.getInstance(instance.definition.key) and container.getProperty(instance.definition.key, "value") == instance.value:
# If the settings match that of the machine profile, just skip since we inherit the machine profile.
continue
self._addSettingElement(builder, instance)
self._addSettingElement(builder, instance)
if material_container.getMetaDataEntry("buildplate_compatible") and not buildplate_dict:
buildplate_dict["buildplate_compatible"] = material_container.getMetaDataEntry("buildplate_compatible")
buildplate_dict["buildplate_recommended"] = material_container.getMetaDataEntry("buildplate_recommended")
buildplate_dict["material_container"] = material_container
builder.end("hotend")
builder.end("hotend")
if buildplate_dict:
for variant_name in buildplate_dict["buildplate_compatible"]:
builder.start("buildplate", {"id": variant_name})
material_container = buildplate_dict["material_container"]
buildplate_compatible_dict = material_container.getMetaDataEntry("buildplate_compatible")
buildplate_recommended_dict = material_container.getMetaDataEntry("buildplate_recommended")
if buildplate_compatible_dict:
compatible = buildplate_compatible_dict[variant_name]
recommended = buildplate_recommended_dict[variant_name]
builder.start("setting", {"key": "hardware compatible"})
builder.data("yes" if compatible else "no")
builder.end("setting")
builder.start("setting", {"key": "hardware recommended"})
builder.data("yes" if recommended else "no")
builder.end("setting")
builder.end("buildplate")
builder.end("machine")
@ -544,7 +597,6 @@ class XmlMaterialProfile(InstanceContainer):
for machine_id in machine_id_list:
definitions = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id)
if not definitions:
Logger.log("w", "No definition found for machine ID %s", machine_id)
continue
definition = definitions[0]
@ -591,14 +643,11 @@ class XmlMaterialProfile(InstanceContainer):
if buildplate_id is None:
continue
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(
id = buildplate_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(
definition = machine_id, name = buildplate_id)
if not variant_containers:
from cura.Machines.VariantManager import VariantType
variant_manager = CuraApplication.getInstance().getVariantManager()
variant_node = variant_manager.getVariantNode(machine_id, buildplate_id,
variant_type = VariantType.BUILD_PLATE)
if not variant_node:
continue
buildplate_compatibility = machine_compatibility
@ -619,16 +668,14 @@ class XmlMaterialProfile(InstanceContainer):
hotends = machine.iterfind("./um:hotend", self.__namespaces)
for hotend in hotends:
hotend_id = hotend.get("id")
if hotend_id is None:
# The "id" field for hotends in material profiles are actually
hotend_name = hotend.get("id")
if hotend_name is None:
continue
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = hotend_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = hotend_id)
if not variant_containers:
variant_manager = CuraApplication.getInstance().getVariantManager()
variant_node = variant_manager.getVariantNode(machine_id, hotend_name)
if not variant_node:
continue
hotend_compatibility = machine_compatibility
@ -644,20 +691,20 @@ class XmlMaterialProfile(InstanceContainer):
else:
Logger.log("d", "Unsupported material setting %s", key)
new_hotend_id = self.getId() + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
new_hotend_specific_material_id = self.getId() + "_" + machine_id + "_" + hotend_name.replace(" ", "_")
# Same as machine compatibility, keep the derived material containers consistent with the parent material
if ContainerRegistry.getInstance().isLoaded(new_hotend_id):
new_hotend_material = ContainerRegistry.getInstance().findContainers(id = new_hotend_id)[0]
if ContainerRegistry.getInstance().isLoaded(new_hotend_specific_material_id):
new_hotend_material = ContainerRegistry.getInstance().findContainers(id = new_hotend_specific_material_id)[0]
is_new_material = False
else:
new_hotend_material = XmlMaterialProfile(new_hotend_id)
new_hotend_material = XmlMaterialProfile(new_hotend_specific_material_id)
is_new_material = True
new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_hotend_material.getMetaData()["id"] = new_hotend_id
new_hotend_material.getMetaData()["id"] = new_hotend_specific_material_id
new_hotend_material.getMetaData()["name"] = self.getName()
new_hotend_material.getMetaData()["variant"] = variant_containers[0]["id"]
new_hotend_material.getMetaData()["variant_name"] = hotend_name
new_hotend_material.setDefinition(machine_id)
# Don't use setMetadata, as that overrides it for all materials with same base file
new_hotend_material.getMetaData()["compatible"] = hotend_compatibility
@ -777,7 +824,6 @@ class XmlMaterialProfile(InstanceContainer):
for machine_id in machine_id_list:
definition_metadata = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id)
if not definition_metadata:
Logger.log("w", "No definition found for machine ID %s", machine_id)
continue
definition_metadata = definition_metadata[0]
@ -787,15 +833,11 @@ class XmlMaterialProfile(InstanceContainer):
if machine_compatibility:
new_material_id = container_id + "_" + machine_id
# The child or derived material container may already exist. This can happen when a material in a
# project file and the a material in Cura have the same ID.
# In the case if a derived material already exists, override that material container because if
# the data in the parent material has been changed, the derived ones should be updated too.
found_materials = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = new_material_id)
if found_materials:
new_material_metadata = found_materials[0]
else:
new_material_metadata = {}
# Do not look for existing container/container metadata with the same ID although they may exist.
# In project loading and perhaps some other places, we only want to get information (metadata)
# from a file without changing the current state of the system. If we overwrite the existing
# metadata here, deserializeMetadata() will not be safe for retrieving information.
new_material_metadata = {}
new_material_metadata.update(base_metadata)
new_material_metadata["id"] = new_material_id
@ -803,8 +845,7 @@ class XmlMaterialProfile(InstanceContainer):
new_material_metadata["machine_manufacturer"] = machine_manufacturer
new_material_metadata["definition"] = machine_id
if len(found_materials) == 0: #This is a new material.
result_metadata.append(new_material_metadata)
result_metadata.append(new_material_metadata)
buildplates = machine.iterfind("./um:buildplate", cls.__namespaces)
buildplate_map = {}
@ -815,15 +856,17 @@ class XmlMaterialProfile(InstanceContainer):
if buildplate_id is None:
continue
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = buildplate_id)
if not variant_containers:
variant_metadata = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = buildplate_id)
if not variant_metadata:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = buildplate_id)
variant_metadata = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = buildplate_id)
if not variant_containers:
if not variant_metadata:
continue
settings = buildplate.iterfind("./um:setting", cls.__namespaces)
buildplate_compatibility = True
buildplate_recommended = True
for entry in settings:
key = entry.get("key")
if key == "hardware compatible":
@ -831,50 +874,36 @@ class XmlMaterialProfile(InstanceContainer):
elif key == "hardware recommended":
buildplate_recommended = cls._parseCompatibleValue(entry.text)
buildplate_map["buildplate_compatible"][buildplate_id] = buildplate_map["buildplate_compatible"]
buildplate_map["buildplate_recommended"][buildplate_id] = buildplate_map["buildplate_recommended"]
buildplate_map["buildplate_compatible"][buildplate_id] = buildplate_compatibility
buildplate_map["buildplate_recommended"][buildplate_id] = buildplate_recommended
for hotend in machine.iterfind("./um:hotend", cls.__namespaces):
hotend_id = hotend.get("id")
if hotend_id is None:
hotend_name = hotend.get("id")
if hotend_name is None:
continue
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = hotend_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = hotend_id)
hotend_compatibility = machine_compatibility
for entry in hotend.iterfind("./um:setting", cls.__namespaces):
key = entry.get("key")
if key == "hardware compatible":
hotend_compatibility = cls._parseCompatibleValue(entry.text)
new_hotend_id = container_id + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
new_hotend_specific_material_id = container_id + "_" + machine_id + "_" + hotend_name.replace(" ", "_")
# Same as machine compatibility, keep the derived material containers consistent with the parent material
found_materials = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = new_hotend_id)
if found_materials:
new_hotend_material_metadata = found_materials[0]
else:
new_hotend_material_metadata = {}
# Same as above, do not overwrite existing metadata.
new_hotend_material_metadata = {}
new_hotend_material_metadata.update(base_metadata)
if variant_containers:
new_hotend_material_metadata["variant"] = variant_containers[0]["id"]
else:
new_hotend_material_metadata["variant"] = hotend_id
_with_missing_variants.append(new_hotend_material_metadata)
new_hotend_material_metadata["variant_name"] = hotend_name
new_hotend_material_metadata["compatible"] = hotend_compatibility
new_hotend_material_metadata["machine_manufacturer"] = machine_manufacturer
new_hotend_material_metadata["id"] = new_hotend_id
new_hotend_material_metadata["id"] = new_hotend_specific_material_id
new_hotend_material_metadata["definition"] = machine_id
if buildplate_map["buildplate_compatible"]:
new_hotend_material_metadata["buildplate_compatible"] = buildplate_map["buildplate_compatible"]
new_hotend_material_metadata["buildplate_recommended"] = buildplate_map["buildplate_recommended"]
if len(found_materials) == 0:
result_metadata.append(new_hotend_material_metadata)
result_metadata.append(new_hotend_material_metadata)
# there is only one ID for a machine. Once we have reached here, it means we have already found
# a workable ID for that machine, so there is no need to continue
@ -916,11 +945,11 @@ class XmlMaterialProfile(InstanceContainer):
else:
merged_name_parts.append(part)
id_list = [name.lower().replace(" ", ""), # simply removing all spaces
id_list = {name.lower().replace(" ", ""), # simply removing all spaces
name.lower().replace(" ", "_"), # simply replacing all spaces with underscores
"_".join(merged_name_parts),
]
}
id_list = list(id_list)
return id_list
## Gets a mapping from product names in the XML files to their definition
@ -954,7 +983,8 @@ class XmlMaterialProfile(InstanceContainer):
"retraction amount": "retraction_amount",
"retraction speed": "retraction_speed",
"adhesion tendency": "material_adhesion_tendency",
"surface energy": "material_surface_energy"
"surface energy": "material_surface_energy",
"shrinkage percentage": "material_shrinkage_percentage",
}
__unmapped_settings = [
"hardware compatible",
@ -994,21 +1024,3 @@ def _indent(elem, level = 0):
# before the last }
def _tag_without_namespace(element):
return element.tag[element.tag.rfind("}") + 1:]
#While loading XML profiles, some of these profiles don't know what variant
#they belong to. We'd like to search by the machine ID and the variant's
#name, but we don't know the variant's ID. Not all variants have been loaded
#yet so we can't run a filter on the name and machine. The ID is unknown
#so we can't lazily load the variant either. So we have to wait until all
#the rest is loaded properly and then assign the correct variant to the
#material files that were missing it.
_with_missing_variants = []
def _fillMissingVariants():
registry = ContainerRegistry.getInstance()
for variant_metadata in _with_missing_variants:
variants = registry.findContainersMetadata(definition = variant_metadata["definition"], name = variant_metadata["variant"])
if not variants:
Logger.log("w", "Could not find variant for variant-specific material {material_id}.".format(material_id = variant_metadata["id"]))
continue
variant_metadata["variant"] = variants[0]["id"]
ContainerRegistry.allMetadataLoaded.connect(_fillMissingVariants)