mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-06 13:34:01 -06:00
Merge branch 'master' into feature_xmlmaterials_cura_settings
This commit is contained in:
commit
a85c42c246
617 changed files with 6736 additions and 2860 deletions
|
@ -16,7 +16,6 @@ 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 cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||
|
@ -81,7 +80,7 @@ class ThreeMFReader(MeshReader):
|
|||
|
||||
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())
|
||||
|
@ -110,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.
|
||||
|
@ -122,7 +119,7 @@ class ThreeMFReader(MeshReader):
|
|||
um_node.callDecoration("setActiveExtruder", default_stack.getId())
|
||||
|
||||
# Get the definition & set it
|
||||
definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack)
|
||||
definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack.definition)
|
||||
um_node.callDecoration("getStack").getTop().setDefinition(definition_id)
|
||||
|
||||
setting_container = um_node.callDecoration("getStack").getTop()
|
||||
|
@ -140,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
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
@ -59,9 +63,9 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
version_file = zipfile.ZipInfo("Cura/version.ini")
|
||||
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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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().getMultiBuildPlateModel().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,18 +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)
|
||||
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
|
||||
|
@ -120,13 +118,6 @@ 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
|
||||
|
||||
Preferences.getInstance().addPreference("general/auto_slice", True)
|
||||
|
@ -141,6 +132,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.
|
||||
|
@ -209,7 +224,11 @@ class CuraEngineBackend(QObject, Backend):
|
|||
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)
|
||||
|
@ -217,9 +236,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)
|
||||
|
||||
|
@ -289,6 +305,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:
|
||||
|
@ -297,6 +314,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
|
||||
|
@ -325,6 +343,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
|
||||
|
@ -347,6 +366,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:
|
||||
|
@ -355,6 +375,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)
|
||||
|
||||
|
@ -364,6 +385,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)
|
||||
self._invokeSlice()
|
||||
|
@ -405,7 +427,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
|
||||
|
@ -432,7 +454,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]
|
||||
|
@ -520,11 +542,9 @@ 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
|
||||
if not self._slicing and self._build_plates_to_be_sliced:
|
||||
self.needsSlicing()
|
||||
self._onChanged()
|
||||
|
@ -550,12 +570,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()
|
||||
|
@ -582,7 +605,12 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
# See if we need to process the sliced layers job.
|
||||
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate:
|
||||
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
|
||||
|
@ -621,7 +649,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()
|
||||
|
@ -707,7 +739,11 @@ class CuraEngineBackend(QObject, Backend):
|
|||
# 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
|
||||
|
@ -773,3 +809,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()
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -129,21 +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
|
||||
|
||||
# Validate settings per selectable model
|
||||
if Application.getInstance().getObjectsModel().stacksHaveErrors():
|
||||
self.setResult(StartJobResult.ObjectSettingError)
|
||||
return
|
||||
|
||||
# Don't slice if there is a per object setting with an error value.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.isSelectable():
|
||||
if not isinstance(node, CuraSceneNode) or not node.isSelectable():
|
||||
continue
|
||||
|
||||
if self._checkStackForErrors(node.callDecoration("getStack")):
|
||||
|
@ -193,11 +191,15 @@ class StartSliceJob(Job):
|
|||
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()
|
||||
|
||||
|
@ -209,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
|
||||
|
||||
|
@ -223,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()
|
||||
|
@ -273,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.
|
||||
|
@ -381,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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
41
plugins/GCodeGzWriter/GCodeGzWriter.py
Normal file
41
plugins/GCodeGzWriter/GCodeGzWriter.py
Normal 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
|
22
plugins/GCodeGzWriter/__init__.py
Normal file
22
plugins/GCodeGzWriter/__init__.py
Normal 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() }
|
8
plugins/GCodeGzWriter/plugin.json
Normal file
8
plugins/GCodeGzWriter/plugin.json
Normal 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"
|
||||
}
|
|
@ -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,6 +66,11 @@ 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:
|
||||
|
|
|
@ -1,17 +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 UM.Util import parseBool
|
||||
|
||||
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.
|
||||
#
|
||||
|
@ -45,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
|
||||
|
@ -94,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.
|
||||
#
|
||||
|
@ -104,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 g-code!")
|
||||
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")
|
||||
|
@ -121,41 +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"))
|
||||
|
||||
# Change the default defintion
|
||||
default_machine_definition = "fdmprinter"
|
||||
if parseBool(stack.getMetaDataEntry("has_machine_quality", "False")):
|
||||
default_machine_definition = stack.getMetaDataEntry("quality_definition")
|
||||
if not default_machine_definition:
|
||||
default_machine_definition = stack.definition.getId()
|
||||
flat_global_container.setMetaDataEntry("definition", default_machine_definition)
|
||||
# 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 defintion
|
||||
flat_extruder_quality.setMetaDataEntry("definition", default_machine_definition)
|
||||
# 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.
|
||||
|
@ -169,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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -244,6 +244,7 @@ Cura.MachineAction
|
|||
height: childrenRect.height
|
||||
width: childrenRect.width
|
||||
text: machineExtruderCountProvider.properties.description
|
||||
visible: extruderCountModel.count >= 2
|
||||
|
||||
Row
|
||||
{
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -888,4 +894,4 @@ Cura.MachineAction
|
|||
watchedProperties: [ "value" ]
|
||||
storeIndex: manager.containerIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,22 +48,17 @@ 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:
|
||||
#If index error occurs, then the icon on monitor button also should be updated
|
||||
self._updateIconSource()
|
||||
pass
|
||||
|
||||
def _onEngineCreated(self):
|
||||
|
@ -82,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")
|
||||
|
@ -92,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"
|
||||
|
|
|
@ -25,20 +25,7 @@ UM.TooltipArea
|
|||
|
||||
onClicked:
|
||||
{
|
||||
// Important first set visible and then subscribe
|
||||
// otherwise the setting is not yet in list
|
||||
// For unsubscribe is important first remove the subscription and then
|
||||
// set as invisible
|
||||
if(checked)
|
||||
{
|
||||
addedSettingsModel.setVisible(model.key, checked);
|
||||
UM.ActiveTool.triggerActionWithData("subscribeForSettingValidation", model.key)
|
||||
}
|
||||
else
|
||||
{
|
||||
UM.ActiveTool.triggerActionWithData("unsubscribeForSettingValidation", model.key)
|
||||
addedSettingsModel.setVisible(model.key, checked);
|
||||
}
|
||||
addedSettingsModel.setVisible(model.key, checked);
|
||||
UM.ActiveTool.forceUpdate();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
@ -240,10 +249,7 @@ Item {
|
|||
width: Math.round(UM.Theme.getSize("setting").height / 2)
|
||||
height: UM.Theme.getSize("setting").height
|
||||
|
||||
onClicked: {
|
||||
addedSettingsModel.setVisible(model.key, false)
|
||||
UM.ActiveTool.triggerActionWithData("unsubscribeForSettingValidation", model.key)
|
||||
}
|
||||
onClicked: addedSettingsModel.setVisible(model.key, false)
|
||||
|
||||
style: ButtonStyle
|
||||
{
|
||||
|
@ -378,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:
|
||||
|
@ -389,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
|
||||
|
@ -404,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
|
||||
|
@ -440,7 +457,7 @@ Item {
|
|||
|
||||
anchors
|
||||
{
|
||||
top: filter.bottom;
|
||||
top: filterInput.bottom;
|
||||
left: parent.left;
|
||||
right: parent.right;
|
||||
bottom: parent.bottom;
|
||||
|
@ -452,10 +469,6 @@ Item {
|
|||
{
|
||||
id: definitionsModel;
|
||||
containerId: Cura.MachineManager.activeDefinitionId
|
||||
filter:
|
||||
{
|
||||
"settable_per_mesh": true
|
||||
}
|
||||
visibilityHandler: UM.SettingPreferenceVisibilityHandler {}
|
||||
expanded: [ "*" ]
|
||||
exclude:
|
||||
|
@ -487,6 +500,7 @@ Item {
|
|||
}
|
||||
}
|
||||
}
|
||||
Component.onCompleted: settingPickDialog.updateFilter()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -510,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
|
||||
|
|
|
@ -10,10 +10,7 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
|||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from UM.Settings.SettingInstance import SettingInstance
|
||||
from UM.Event import Event
|
||||
from UM.Settings.Validator import ValidatorState
|
||||
from UM.Logger import Logger
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
## This tool allows the user to add & change settings per node in the scene.
|
||||
# The settings per object are kept in a ContainerStack, which is linked to a node by decorator.
|
||||
|
@ -37,12 +34,6 @@ class PerObjectSettingsTool(Tool):
|
|||
self._onGlobalContainerChanged()
|
||||
Selection.selectionChanged.connect(self._updateEnabled)
|
||||
|
||||
self._scene = Application.getInstance().getController().getScene()
|
||||
|
||||
self._error_check_timer = QTimer()
|
||||
self._error_check_timer.setInterval(250)
|
||||
self._error_check_timer.setSingleShot(True)
|
||||
self._error_check_timer.timeout.connect(self._updateStacksHaveErrors)
|
||||
|
||||
def event(self, event):
|
||||
super().event(event)
|
||||
|
@ -151,65 +142,3 @@ class PerObjectSettingsTool(Tool):
|
|||
else:
|
||||
self._single_model_selected = True
|
||||
Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, self._advanced_mode and self._single_model_selected)
|
||||
|
||||
|
||||
def _onPropertyChanged(self, key: str, property_name: str) -> None:
|
||||
if property_name == "validationState":
|
||||
# self._error_check_timer.start()
|
||||
return
|
||||
|
||||
def _updateStacksHaveErrors(self) -> None:
|
||||
return
|
||||
# self._checkStacksHaveErrors()
|
||||
|
||||
|
||||
def _checkStacksHaveErrors(self):
|
||||
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
|
||||
# valdiate only objects which can be selected because the settings per object
|
||||
# can be applied only for them
|
||||
if not node.isSelectable():
|
||||
continue
|
||||
|
||||
hasErrors = self._checkStackForErrors(node.callDecoration("getStack"))
|
||||
Application.getInstance().getObjectsModel().setStacksHaveErrors(hasErrors)
|
||||
|
||||
#If any of models has an error then no reason check next objects on the build plate
|
||||
if hasErrors:
|
||||
break
|
||||
|
||||
|
||||
def _checkStackForErrors(self, stack):
|
||||
print("checking for errors")
|
||||
if stack is None:
|
||||
return False
|
||||
|
||||
for key in stack.getAllKeys():
|
||||
validation_state = stack.getProperty(key, "validationState")
|
||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
|
||||
Logger.log("w", "Setting Per Object %s is not valid.", key)
|
||||
return True
|
||||
return False
|
||||
|
||||
def subscribeForSettingValidation(self, setting_name):
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
stack = selected_object.callDecoration("getStack") # Don't try to get the active extruder since it may be None anyway.
|
||||
if not stack:
|
||||
return ""
|
||||
|
||||
settings = stack.getTop()
|
||||
setting_instance = settings.getInstance(setting_name)
|
||||
if setting_instance:
|
||||
setting_instance.propertyChanged.connect(self._onPropertyChanged)
|
||||
|
||||
def unsubscribeForSettingValidation(self, setting_name):
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
stack = selected_object.callDecoration("getStack") # Don't try to get the active extruder since it may be None anyway.
|
||||
if not stack:
|
||||
return ""
|
||||
|
||||
settings = stack.getTop()
|
||||
setting_instance = settings.getInstance(setting_name)
|
||||
if setting_instance:
|
||||
setting_instance.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
|
|
|
@ -173,7 +173,10 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
Logger.log("d", "Creating post processing plugin view.")
|
||||
|
||||
## Load all scripts in the scripts folders
|
||||
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Preferences)]:
|
||||
# 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:
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -1,39 +1,101 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Tool import Tool
|
||||
from PyQt5.QtCore import Qt, QUrl
|
||||
from UM.Application import Application
|
||||
from UM.Event import Event
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||
from UM.Settings.SettingInstance import SettingInstance
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
|
||||
import os
|
||||
import os.path
|
||||
|
||||
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")
|
||||
|
@ -41,31 +103,61 @@ class SupportEraser(Tool):
|
|||
mesh = MeshBuilder()
|
||||
mesh.addCube(10,10,10)
|
||||
node.setMeshData(mesh.build())
|
||||
# Place the cube in the platform. Do it manually so it works if the "automatic drop models" is OFF
|
||||
move_vector = Vector(0, 5, 0)
|
||||
node.setPosition(move_vector)
|
||||
|
||||
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||
|
||||
node.addDecorator(SettingOverrideDecorator())
|
||||
node.addDecorator(BuildPlateDecorator(active_build_plate))
|
||||
node.addDecorator(SliceableObjectDecorator())
|
||||
|
||||
stack = node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
||||
if not stack:
|
||||
node.addDecorator(SettingOverrideDecorator())
|
||||
stack = node.callDecoration("getStack")
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -79,27 +85,51 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
|||
|
||||
self._latest_reply_handler = None
|
||||
|
||||
|
||||
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
|
||||
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().getMultiBuildPlateModel().activeBuildPlate
|
||||
gcode_list = gcode_dict[active_build_plate_id]
|
||||
|
||||
if not gcode_list:
|
||||
# Unable to find g-code. Nothing to send
|
||||
return
|
||||
|
||||
self._gcode = gcode_list
|
||||
|
||||
if len(self._printers) > 1:
|
||||
self._spawnPrinterSelectionDialog()
|
||||
#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()
|
||||
|
||||
#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)
|
||||
|
||||
# Notify the UI that a switch to the print monitor should happen
|
||||
Application.getInstance().getController().setActiveStage("MonitorStage")
|
||||
if is_job_sent:
|
||||
Application.getInstance().getController().setActiveStage("MonitorStage")
|
||||
|
||||
def _spawnPrinterSelectionDialog(self):
|
||||
if self._printer_selection_dialog is None:
|
||||
|
@ -112,29 +142,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 = []
|
||||
|
||||
|
@ -146,18 +201,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")
|
||||
|
||||
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()
|
||||
|
@ -169,7 +233,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
|
||||
|
@ -182,7 +246,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
|||
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()
|
||||
|
@ -194,32 +258,33 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
|||
# the "reply" should be disconnected
|
||||
if self._latest_reply_handler:
|
||||
self._latest_reply_handler.disconnect()
|
||||
self._latest_reply_handler = None
|
||||
|
||||
|
||||
@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 or print_job.state == "queued"]
|
||||
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):
|
||||
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:
|
||||
|
@ -232,22 +297,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:
|
||||
|
@ -270,13 +335,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
|
||||
|
||||
|
@ -296,8 +361,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"])
|
||||
|
@ -318,7 +383,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
|
||||
|
||||
|
@ -347,34 +412,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:
|
||||
|
@ -411,7 +487,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
|||
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
|
||||
|
||||
|
@ -422,7 +498,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
|
|
@ -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()
|
|
@ -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)
|
||||
|
|
|
@ -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"}
|
||||
|
|
|
@ -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()]
|
44
plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py
Normal file
44
plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py
Normal 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 }
|
8
plugins/VersionUpgrade/VersionUpgrade32to33/plugin.json
Normal file
8
plugins/VersionUpgrade/VersionUpgrade32to33/plugin.json
Normal 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"
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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];
|
||||
|
||||
|
|
|
@ -201,31 +201,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()._variant_manager
|
||||
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_name = container.getMetaDataEntry("variant_name")
|
||||
if variant_name:
|
||||
machine_nozzle_map[definition_id][variant_name] = variant_manager.getVariantNode(definition_id,
|
||||
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
|
||||
|
@ -234,7 +237,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():
|
||||
|
@ -244,41 +248,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_name, variant_node in machine_nozzle_map[definition_id].items():
|
||||
# The hotend identifier is not the containers name, but its "name".
|
||||
builder.start("hotend", {"id": hotend_name})
|
||||
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})
|
||||
|
||||
# Compatible is a special case, as it's added as a meta data entry (instead of an instance).
|
||||
compatible = variant_node.metadata.get("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).
|
||||
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")
|
||||
|
||||
for instance in variant_node.getContainer().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 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
|
||||
|
||||
self._addSettingElement(builder, instance)
|
||||
self._addSettingElement(builder, instance)
|
||||
|
||||
builder.end("hotend")
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
|
@ -622,7 +659,8 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
|
||||
from cura.Machines.VariantManager import VariantType
|
||||
variant_manager = CuraApplication.getInstance().getVariantManager()
|
||||
variant_node = variant_manager.getVariantNode(machine_id, buildplate_id)
|
||||
variant_node = variant_manager.getVariantNode(machine_id, buildplate_id,
|
||||
variant_type = VariantType.BUILD_PLATE)
|
||||
if not variant_node:
|
||||
continue
|
||||
|
||||
|
@ -815,15 +853,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
|
||||
|
@ -831,8 +865,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 = {}
|
||||
|
@ -843,15 +876,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":
|
||||
|
@ -859,8 +894,8 @@ 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_name = hotend.get("id")
|
||||
|
@ -875,12 +910,8 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
|
||||
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_specific_material_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)
|
||||
new_hotend_material_metadata["variant_name"] = hotend_name
|
||||
|
@ -892,8 +923,7 @@ class XmlMaterialProfile(InstanceContainer):
|
|||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue