Merge branch 'master' into fix_per_meshgroup_settings

This commit is contained in:
fieldOfView 2018-03-15 10:40:18 +01:00
commit 8fe2077413
803 changed files with 42122 additions and 26272 deletions

View file

@ -1,29 +1,31 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
import zipfile
import numpy
import Savitar
from UM.Application import Application
from UM.Logger import Logger
from UM.Math.Matrix import Matrix
from UM.Math.Vector import Vector
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Mesh.MeshReader import MeshReader
from UM.Scene.GroupDecorator import GroupDecorator
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
from UM.Application import Application
from cura.Settings.ExtruderManager import ExtruderManager
from cura.QualityManager import QualityManager
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
MYPY = False
import Savitar
import numpy
try:
if not MYPY:
import xml.etree.cElementTree as ET
@ -37,10 +39,6 @@ class ThreeMFReader(MeshReader):
super().__init__()
self._supported_extensions = [".3mf"]
self._root = None
self._namespaces = {
"3mf": "http://schemas.microsoft.com/3dmanufacturing/core/2015/02",
"cura": "http://software.ultimaker.com/xml/cura/3mf/2015/10"
}
self._base_name = ""
self._unit = None
self._object_count = 0 # Used to name objects as there is no node name yet.
@ -81,7 +79,7 @@ class ThreeMFReader(MeshReader):
self._object_count += 1
node_name = "Object %s" % self._object_count
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
um_node = CuraSceneNode()
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
@ -124,8 +122,8 @@ class ThreeMFReader(MeshReader):
um_node.callDecoration("setActiveExtruder", default_stack.getId())
# Get the definition & set it
definition = QualityManager.getInstance().getParentMachineDefinition(global_container_stack.getBottom())
um_node.callDecoration("getStack").getTop().setDefinition(definition.getId())
definition_id = getMachineDefinitionIDForQualitySearch(global_container_stack.definition)
um_node.callDecoration("getStack").getTop().setDefinition(definition_id)
setting_container = um_node.callDecoration("getStack").getTop()

File diff suppressed because it is too large Load diff

View file

@ -52,7 +52,6 @@ class WorkspaceDialog(QObject):
machineConflictChanged = pyqtSignal()
qualityChangesConflictChanged = pyqtSignal()
definitionChangesConflictChanged = pyqtSignal()
materialConflictChanged = pyqtSignal()
numVisibleSettingsChanged = pyqtSignal()
activeModeChanged = pyqtSignal()
@ -196,10 +195,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 +224,11 @@ class WorkspaceDialog(QObject):
self._has_quality_changes_conflict = quality_changes_conflict
self.qualityChangesConflictChanged.emit()
def setDefinitionChangesConflict(self, definition_changes_conflict):
if self._has_definition_changes_conflict != definition_changes_conflict:
self._has_definition_changes_conflict = definition_changes_conflict
self.definitionChangesConflictChanged.emit()
def getResult(self):
if "machine" in self._result and not self._has_machine_conflict:
self._result["machine"] = None
if "quality_changes" in self._result and not self._has_quality_changes_conflict:
self._result["quality_changes"] = None
if "definition_changes" in self._result and not self._has_definition_changes_conflict:
self._result["definition_changes"] = None
if "material" in self._result and not self._has_material_conflict:
self._result["material"] = None

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -10,7 +10,6 @@ from UM.Logger import Logger
from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Settings.Validator import ValidatorState #To find if a setting is in an error state. We can't slice then.
from UM.Platform import Platform
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Qt.Duration import DurationFormat
@ -32,7 +31,11 @@ import Arcus
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class CuraEngineBackend(QObject, Backend):
backendError = Signal()
## Starts the back-end plug-in.
#
# This registers all the signal listeners and prepares for communication
@ -59,23 +62,26 @@ class CuraEngineBackend(QObject, Backend):
default_engine_location = execpath
break
self._application = Application.getInstance()
self._multi_build_plate_model = None
self._machine_error_checker = None
if not default_engine_location:
raise EnvironmentError("Could not find CuraEngine")
Logger.log("i", "Found CuraEngine at: %s" %(default_engine_location))
Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
default_engine_location = os.path.abspath(default_engine_location)
Preferences.getInstance().addPreference("backend/location", default_engine_location)
# Workaround to disable layer view processing if layer view is not active.
self._layer_view_active = False
Application.getInstance().getController().activeViewChanged.connect(self._onActiveViewChanged)
Application.getInstance().getBuildPlateModel().activeBuildPlateChanged.connect(self._onActiveViewChanged)
self._onActiveViewChanged()
self._stored_layer_data = []
self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
self._scene = Application.getInstance().getController().getScene()
self._scene = self._application.getController().getScene()
self._scene.sceneChanged.connect(self._onSceneChanged)
# Triggers for auto-slicing. Auto-slicing is triggered as follows:
@ -83,19 +89,10 @@ class CuraEngineBackend(QObject, Backend):
# - whenever there is a value change, we start the timer
# - sometimes an error check can get scheduled for a value change, in that case, we ONLY want to start the
# auto-slicing timer when that error check is finished
# If there is an error check, it will set the "_is_error_check_scheduled" flag, stop the auto-slicing timer,
# and only wait for the error check to be finished to start the auto-slicing timer again.
# If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished
# to start the auto-slicing timer again.
#
self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
Application.getInstance().getExtruderManager().extrudersAdded.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Application.getInstance().stacksValidationFinished.connect(self._onStackErrorCheckFinished)
# A flag indicating if an error check was scheduled
# If so, we will stop the auto-slice timer and start upon the error check
self._is_error_check_scheduled = False
# Listeners for receiving messages from the back-end.
self._message_handlers["cura.proto.Layer"] = self._onLayerMessage
@ -121,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)
@ -142,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.
@ -187,14 +201,12 @@ class CuraEngineBackend(QObject, Backend):
## Manually triggers a reslice
@pyqtSlot()
def forceSlice(self):
if self._use_timer:
self._change_timer.start()
else:
self.slice()
self.markSliceAll()
self.slice()
## Perform a slice of the scene.
def slice(self):
Logger.log("d", "starting to slice!")
Logger.log("d", "Starting to slice...")
self._slice_start_time = time()
if not self._build_plates_to_be_sliced:
self.processingProgress.emit(1.0)
@ -202,14 +214,14 @@ class CuraEngineBackend(QObject, Backend):
return
if self._process_layers_job:
Logger.log("d", " ## Process layers job still busy, trying later")
Logger.log("d", "Process layers job still busy, trying later.")
return
if not hasattr(self._scene, "gcode_dict"):
self._scene.gcode_dict = {}
# see if we really have to slice
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0)
Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced)
num_objects = self._numObjects()
@ -292,6 +304,7 @@ class CuraEngineBackend(QObject, Backend):
if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error:
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
return
if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible:
@ -300,6 +313,7 @@ class CuraEngineBackend(QObject, Backend):
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
else:
self.backendStateChange.emit(BackendState.NotStarted)
return
@ -328,6 +342,7 @@ class CuraEngineBackend(QObject, Backend):
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
else:
self.backendStateChange.emit(BackendState.NotStarted)
return
@ -350,6 +365,7 @@ class CuraEngineBackend(QObject, Backend):
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
return
if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError:
@ -358,6 +374,7 @@ class CuraEngineBackend(QObject, Backend):
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
else:
self.backendStateChange.emit(BackendState.NotStarted)
@ -367,9 +384,9 @@ class CuraEngineBackend(QObject, Backend):
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
self.backendStateChange.emit(BackendState.Error)
self.backendError.emit(job)
else:
self.backendStateChange.emit(BackendState.NotStarted)
pass
self._invokeSlice()
return
@ -427,12 +444,10 @@ class CuraEngineBackend(QObject, Backend):
if not isinstance(source, SceneNode):
return
# This case checks if the source node is a node that contains a GCode. In this case the
# cached layer data is removed so the previous data is not rendered - CURA-4821
# This case checks if the source node is a node that contains GCode. In this case the
# current layer data is removed so the previous data is not rendered - CURA-4821
if source.callDecoration("isBlockSlicing") and source.callDecoration("getLayerData"):
if self._stored_optimized_layer_data:
print(self._stored_optimized_layer_data)
del self._stored_optimized_layer_data[source.callDecoration("getBuildPlateNumber")]
self._stored_optimized_layer_data = {}
build_plate_changed = set()
source_build_plate_number = source.callDecoration("getBuildPlateNumber")
@ -502,7 +517,7 @@ class CuraEngineBackend(QObject, Backend):
node.getParent().removeChild(node)
def markSliceAll(self):
for build_plate_number in range(Application.getInstance().getBuildPlateModel().maxBuildPlate + 1):
for build_plate_number in range(Application.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1):
if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number)
@ -526,11 +541,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()
@ -545,6 +558,8 @@ class CuraEngineBackend(QObject, Backend):
#
# \param message The protobuf message containing sliced layer data.
def _onOptimizedLayerMessage(self, message):
if self._start_slice_job_build_plate not in self._stored_optimized_layer_data:
self._stored_optimized_layer_data[self._start_slice_job_build_plate] = []
self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message)
## Called when a progress message is received from the engine.
@ -554,12 +569,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()
@ -585,7 +603,7 @@ class CuraEngineBackend(QObject, Backend):
Logger.log("d", "Slicing took %s seconds", time() - self._slice_start_time )
# See if we need to process the sliced layers job.
active_build_plate = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
if self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and active_build_plate == self._start_slice_job_build_plate:
self._startProcessSlicedLayersJob(active_build_plate)
# self._onActiveViewChanged()
@ -625,7 +643,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()
@ -705,7 +727,7 @@ class CuraEngineBackend(QObject, Backend):
application = Application.getInstance()
view = application.getController().getActiveView()
if view:
active_build_plate = application.getBuildPlateModel().activeBuildPlate
active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate
if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet.
self._layer_view_active = True
# There is data and we're not slicing at the moment
@ -777,3 +799,9 @@ class CuraEngineBackend(QObject, Backend):
def tickle(self):
if self._use_timer:
self._change_timer.start()
def _extruderChanged(self):
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
if build_plate_number not in self._build_plates_to_be_sliced:
self._build_plates_to_be_sliced.append(build_plate_number)
self._invokeSlice()

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import numpy
from string import Formatter
from enum import IntEnum
import time
import re
from UM.Job import Job
from UM.Application import Application
@ -15,7 +16,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.Validator import ValidatorState
from UM.Settings.SettingRelation import RelationType
from cura.Scene.CuraSceneNode import CuraSceneNode as SceneNode
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.OneAtATimeIterator import OneAtATimeIterator
from cura.Settings.ExtruderManager import ExtruderManager
@ -54,19 +55,19 @@ class GcodeStartEndFormatter(Formatter):
extruder_nr = int(kwargs["-1"][key_fragments[1]]) # get extruder_nr values from the global stack
except (KeyError, ValueError):
# either the key does not exist, or the value is not an int
Logger.log("w", "Unable to determine stack nr '%s' for key '%s' in start/end gcode, using global stack", key_fragments[1], key_fragments[0])
Logger.log("w", "Unable to determine stack nr '%s' for key '%s' in start/end g-code, using global stack", key_fragments[1], key_fragments[0])
elif len(key_fragments) != 1:
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end gcode", key)
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end g-code", key)
return "{" + str(key) + "}"
key = key_fragments[0]
try:
return kwargs[str(extruder_nr)][key]
except KeyError:
Logger.log("w", "Unable to replace '%s' placeholder in start/end gcode", key)
Logger.log("w", "Unable to replace '%s' placeholder in start/end g-code", key)
return "{" + key + "}"
else:
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end gcode", key)
Logger.log("w", "Incorrectly formatted placeholder '%s' in start/end g-code", key)
return "{" + str(key) + "}"
@ -128,16 +129,19 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.MaterialIncompatible)
return
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()):
for position, extruder_stack in stack.extruders.items():
material = extruder_stack.findContainer({"type": "material"})
if not extruder_stack.isEnabled:
continue
if material:
if material.getMetaDataEntry("compatible") == False:
self.setResult(StartJobResult.MaterialIncompatible)
return
# Don't slice if there is a per object setting with an error value.
for node in DepthFirstIterator(self._scene.getRoot()):
if type(node) is not SceneNode or not node.isSelectable():
if not isinstance(node, CuraSceneNode) or not node.isSelectable():
continue
if self._checkStackForErrors(node.callDecoration("getStack")):
@ -161,10 +165,15 @@ class StartSliceJob(Job):
if getattr(node, "_outside_buildarea", False):
continue
# Filter on current build plate
build_plate_number = node.callDecoration("getBuildPlateNumber")
if build_plate_number is not None and build_plate_number != self._build_plate_number:
continue
children = node.getAllChildren()
children.append(node)
for child_node in children:
if type(child_node) is SceneNode and child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
if child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
temp_list.append(child_node)
if temp_list:
@ -176,17 +185,24 @@ class StartSliceJob(Job):
temp_list = []
has_printing_mesh = False
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("isSliceable") and type(node) is SceneNode and node.getMeshData() and node.getMeshData().getVertices() is not None:
if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None:
per_object_stack = node.callDecoration("getStack")
is_non_printing_mesh = False
if per_object_stack:
is_non_printing_mesh = any(per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS)
if (node.callDecoration("getBuildPlateNumber") == self._build_plate_number):
if not getattr(node, "_outside_buildarea", False) or is_non_printing_mesh:
temp_list.append(node)
if not is_non_printing_mesh:
has_printing_mesh = True
# Find a reason not to add the node
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:
continue
if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh:
continue
node_position = node.callDecoration("getActiveExtruderPosition")
if not stack.extruders[str(node_position)].isEnabled:
continue
temp_list.append(node)
if not is_non_printing_mesh:
has_printing_mesh = True
Job.yieldThread()
@ -262,9 +278,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.
@ -302,7 +324,7 @@ class StartSliceJob(Job):
settings["default_extruder_nr"] = default_extruder_nr
return str(fmt.format(value, **settings))
except:
Logger.logException("w", "Unable to do token replacement on start/end gcode")
Logger.logException("w", "Unable to do token replacement on start/end g-code")
return str(value)
## Create extruder message from stack
@ -338,10 +360,12 @@ class StartSliceJob(Job):
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
start_gcode = settings["machine_start_gcode"]
bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"}
settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings))
print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"}
settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings))
bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"]
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr}
settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None
print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"]
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr}
settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None
# Replace the setting tokens in start and end g-code.
# Use values from the first used extruder by default so we get the expected temperatures
@ -368,11 +392,11 @@ class StartSliceJob(Job):
# limit_to_extruder property.
def _buildGlobalInheritsStackMessage(self, stack):
for key in stack.getAllKeys():
extruder = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
if extruder >= 0: #Set to a specific extruder.
extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder"))))
if extruder_position >= 0: # Set to a specific extruder.
setting_extruder = self._slice_message.addRepeatedMessage("limit_to_extruder")
setting_extruder.name = key
setting_extruder.extruder = extruder
setting_extruder.extruder = extruder_position
Job.yieldThread()
## Check if a node has per object settings and ensure that they are set correctly in the message

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -437,7 +437,7 @@ class FlavorParser:
scene_node.addDecorator(gcode_list_decorator)
# gcode_dict stores gcode_lists for a number of build plates.
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_dict = {active_build_plate_id: gcode_list}
Application.getInstance().getController().getScene().gcode_dict = gcode_dict

View file

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

View file

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

View file

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

View file

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

View file

@ -125,7 +125,10 @@ class LegacyProfileReader(ProfileReader):
Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?")
return None
current_printer_definition = global_container_stack.definition
profile.setDefinition(current_printer_definition.getId())
quality_definition = current_printer_definition.getMetaDataEntry("quality_definition")
if not quality_definition:
quality_definition = current_printer_definition.getId()
profile.setDefinition(quality_definition)
for new_setting in dict_of_doom["translation"]: # Evaluate all new settings that would get a value from the translations.
old_setting_expression = dict_of_doom["translation"][new_setting]
compiled = compile(old_setting_expression, new_setting, "eval")
@ -162,20 +165,21 @@ class LegacyProfileReader(ProfileReader):
data = stream.getvalue()
profile.deserialize(data)
# The definition can get reset to fdmprinter during the deserialization's upgrade. Here we set the definition
# again.
profile.setDefinition(quality_definition)
#We need to return one extruder stack and one global stack.
global_container_id = container_registry.uniqueName("Global Imported Legacy Profile")
global_profile = profile.duplicate(new_id = global_container_id, new_name = profile_id) #Needs to have the same name as the extruder profile.
global_profile.setDirty(True)
#Only the extruder stack has an extruder metadata entry.
profile.addMetaDataEntry("extruder", ExtruderManager.getInstance().getActiveExtruderStack().definition.getId())
profile_definition = "fdmprinter"
from UM.Util import parseBool
if parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", "False")):
profile_definition = global_container_stack.getMetaDataEntry("quality_definition")
if not profile_definition:
profile_definition = global_container_stack.definition.getId()
global_profile.setDefinition(profile_definition)
#Split all settings into per-extruder and global settings.
for setting_key in profile.getAllKeys():
settable_per_extruder = global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_extruder:
global_profile.removeInstance(setting_key)
else:
profile.removeInstance(setting_key)
return [global_profile, profile]
return [global_profile]

View file

@ -2,20 +2,16 @@
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal
import UM.i18n
from UM.FlameProfiler import pyqtSlot
from cura.MachineAction import MachineAction
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger
from cura.Settings.ExtruderManager import ExtruderManager
from cura.MachineAction import MachineAction
from cura.Settings.CuraStackBuilder import CuraStackBuilder
import UM.i18n
catalog = UM.i18n.i18nCatalog("cura")
@ -26,6 +22,8 @@ class MachineSettingsAction(MachineAction):
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
self._qml_url = "MachineSettingsAction.qml"
self._application = Application.getInstance()
self._global_container_stack = None
from cura.Settings.CuraContainerStack import _ContainerIndexes
@ -34,38 +32,44 @@ class MachineSettingsAction(MachineAction):
self._container_registry = ContainerRegistry.getInstance()
self._container_registry.containerAdded.connect(self._onContainerAdded)
self._container_registry.containerRemoved.connect(self._onContainerRemoved)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._empty_container = self._container_registry.getEmptyInstanceContainer()
self._backend = self._application.getBackend()
self._backend = Application.getInstance().getBackend()
self._empty_definition_container_id_list = []
def _isEmptyDefinitionChanges(self, container_id: str):
if not self._empty_definition_container_id_list:
self._empty_definition_container_id_list = [self._application.empty_container.getId(),
self._application.empty_definition_changes_container.getId()]
return container_id in self._empty_definition_container_id_list
def _onContainerAdded(self, container):
# Add this action as a supported action to all machine definitions
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
Application.getInstance().getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
def _onContainerRemoved(self, container):
# Remove definition_changes containers when a stack is removed
if container.getMetaDataEntry("type") in ["machine", "extruder_train"]:
definition_changes_container = container.definitionChanges
if definition_changes_container == self._empty_container:
definition_changes_id = container.definitionChanges.getId()
if self._isEmptyDefinitionChanges(definition_changes_id):
return
self._container_registry.removeContainer(definition_changes_container.getId())
self._container_registry.removeContainer(definition_changes_id)
def _reset(self):
if not self._global_container_stack:
return
# Make sure there is a definition_changes container to store the machine settings
definition_changes_container = self._global_container_stack.definitionChanges
if definition_changes_container == self._empty_container:
definition_changes_container = CuraStackBuilder.createDefinitionChangesContainer(
self._global_container_stack, self._global_container_stack.getName() + "_settings")
definition_changes_id = self._global_container_stack.definitionChanges.getId()
if self._isEmptyDefinitionChanges(definition_changes_id):
CuraStackBuilder.createDefinitionChangesContainer(self._global_container_stack,
self._global_container_stack.getName() + "_settings")
# Notify the UI in which container to store the machine settings data
from cura.Settings.CuraContainerStack import CuraContainerStack, _ContainerIndexes
from cura.Settings.CuraContainerStack import _ContainerIndexes
container_index = _ContainerIndexes.DefinitionChanges
if container_index != self._container_index:
@ -105,66 +109,15 @@ class MachineSettingsAction(MachineAction):
@pyqtSlot(int)
def setMachineExtruderCount(self, extruder_count):
extruder_manager = Application.getInstance().getExtruderManager()
definition_changes_container = self._global_container_stack.definitionChanges
if not self._global_container_stack or definition_changes_container == self._empty_container:
return
previous_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
if extruder_count == previous_extruder_count:
return
# reset all extruder number settings whose value is no longer valid
for setting_instance in self._global_container_stack.userChanges.findInstances():
setting_key = setting_instance.definition.key
if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"):
continue
old_value = int(self._global_container_stack.userChanges.getProperty(setting_key, "value"))
if old_value >= extruder_count:
self._global_container_stack.userChanges.removeInstance(setting_key)
Logger.log("d", "Reset [%s] because its old value [%s] is no longer valid ", setting_key, old_value)
# Check to see if any objects are set to print with an extruder that will no longer exist
root_node = Application.getInstance().getController().getScene().getRoot()
for node in DepthFirstIterator(root_node):
if node.getMeshData():
extruder_nr = node.callDecoration("getActiveExtruderPosition")
if extruder_nr is not None and int(extruder_nr) > extruder_count - 1:
node.callDecoration("setActiveExtruder", extruder_manager.getExtruderStack(extruder_count - 1).getId())
definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count)
# Make sure one of the extruder stacks is active
extruder_manager.setActiveExtruderIndex(0)
# Move settable_per_extruder values out of the global container
# After CURA-4482 this should not be the case anymore, but we still want to support older project files.
global_user_container = self._global_container_stack.getTop()
if previous_extruder_count == 1:
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
global_user_container = self._global_container_stack.getTop()
for setting_instance in global_user_container.findInstances():
setting_key = setting_instance.definition.key
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_extruder:
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
extruder_stack = extruder_stacks[max(0, limit_to_extruder)]
extruder_stack.getTop().setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
global_user_container.removeInstance(setting_key)
self.forceUpdate()
# Note: this method was in this class before, but since it's quite generic and other plugins also need it
# it was moved to the machine manager instead. Now this method just calls the machine manager.
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):
@ -177,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)
@ -187,101 +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
if not self._global_container_stack:
return
if not self._global_container_stack.getMetaDataEntry("has_materials", False):
return
extruder_stack = self._global_container_stack.extruders[str(extruder_position)]
material_diameter = extruder_stack.material.getProperty("material_diameter", "value")
if not material_diameter:
# in case of "empty" material
material_diameter = 0
material_approximate_diameter = str(round(material_diameter))
machine_diameter = extruder_stack.definitionChanges.getProperty("material_diameter", "value")
if not machine_diameter:
if extruder_stack.definition.hasProperty("material_diameter", "value"):
machine_diameter = extruder_stack.definition.getProperty("material_diameter", "value")
else:
machine_diameter = self._global_container_stack.definition.getProperty("material_diameter", "value")
machine_approximate_diameter = str(round(machine_diameter))
if material_approximate_diameter != machine_approximate_diameter:
Logger.log("i", "The the currently active material(s) do not match the diameter set for the printer. Finding alternatives.")
if self._global_container_stack.getMetaDataEntry("has_machine_materials", False):
materials_definition = self._global_container_stack.definition.getId()
has_material_variants = self._global_container_stack.getMetaDataEntry("has_variants", False)
else:
materials_definition = "fdmprinter"
has_material_variants = False
old_material = extruder_stack.material
search_criteria = {
"type": "material",
"approximate_diameter": machine_approximate_diameter,
"material": old_material.getMetaDataEntry("material", "value"),
"brand": old_material.getMetaDataEntry("brand", "value"),
"supplier": old_material.getMetaDataEntry("supplier", "value"),
"color_name": old_material.getMetaDataEntry("color_name", "value"),
"definition": materials_definition
}
if has_material_variants:
search_criteria["variant"] = extruder_stack.variant.getId()
if old_material == self._empty_container:
search_criteria.pop("material", None)
search_criteria.pop("supplier", None)
search_criteria.pop("brand", None)
search_criteria.pop("definition", None)
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Same material with new diameter is not found, search for generic version of the same material type
search_criteria.pop("supplier", None)
search_criteria.pop("brand", None)
search_criteria["color_name"] = "Generic"
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Generic material with new diameter is not found, search for preferred material
search_criteria.pop("color_name", None)
search_criteria.pop("material", None)
search_criteria["id"] = extruder_stack.getMetaDataEntry("preferred_material")
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Preferred material with new diameter is not found, search for any material
search_criteria.pop("id", None)
materials = self._container_registry.findInstanceContainers(**search_criteria)
if not materials:
# Just use empty material as a final fallback
materials = [self._empty_container]
Logger.log("i", "Selecting new material: %s", materials[0].getId())
extruder_stack.material = materials[0]
self._application.getExtruderManager().updateMaterialForDiameter(extruder_position)

View file

@ -70,8 +70,8 @@ Cura.MachineAction
anchors.top: pageTitle.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
property real columnWidth: ((width - 3 * UM.Theme.getSize("default_margin").width) / 2) | 0
property real labelColumnWidth: columnWidth * 0.5
property real columnWidth: Math.round((width - 3 * UM.Theme.getSize("default_margin").width) / 2)
property real labelColumnWidth: Math.round(columnWidth / 2)
Tab
{
@ -165,7 +165,7 @@ Cura.MachineAction
id: gcodeFlavorComboBox
sourceComponent: comboBoxWithOptions
property string settingKey: "machine_gcode_flavor"
property string label: catalog.i18nc("@label", "Gcode flavor")
property string label: catalog.i18nc("@label", "G-code flavor")
property bool forceUpdateOnChange: true
property var afterOnActivate: manager.updateHasMaterialsMetadata
}
@ -244,6 +244,7 @@ Cura.MachineAction
height: childrenRect.height
width: childrenRect.width
text: machineExtruderCountProvider.properties.description
visible: extruderCountModel.count >= 2
Row
{
@ -307,7 +308,7 @@ Cura.MachineAction
width: settingsTabs.columnWidth
Label
{
text: catalog.i18nc("@label", "Start Gcode")
text: catalog.i18nc("@label", "Start G-code")
font.bold: true
}
Loader
@ -317,7 +318,7 @@ Cura.MachineAction
property int areaWidth: parent.width
property int areaHeight: parent.height - y
property string settingKey: "machine_start_gcode"
property string tooltip: catalog.i18nc("@tooltip", "Gcode commands to be executed at the very start.")
property string tooltip: catalog.i18nc("@tooltip", "G-code commands to be executed at the very start.")
}
}
@ -326,7 +327,7 @@ Cura.MachineAction
width: settingsTabs.columnWidth
Label
{
text: catalog.i18nc("@label", "End Gcode")
text: catalog.i18nc("@label", "End G-code")
font.bold: true
}
Loader
@ -336,7 +337,7 @@ Cura.MachineAction
property int areaWidth: parent.width
property int areaHeight: parent.height - y
property string settingKey: "machine_end_gcode"
property string tooltip: catalog.i18nc("@tooltip", "Gcode commands to be executed at the very end.")
property string tooltip: catalog.i18nc("@tooltip", "G-code commands to be executed at the very end.")
}
}
}
@ -381,6 +382,11 @@ Cura.MachineAction
property string settingKey: "machine_nozzle_size"
property string label: catalog.i18nc("@label", "Nozzle size")
property string unit: catalog.i18nc("@label", "mm")
function afterOnEditingFinished()
{
// Somehow the machine_nozzle_size dependent settings are not updated otherwise
Cura.MachineManager.forceUpdateAllSettings()
}
property bool isExtruderSetting: true
}
@ -390,7 +396,7 @@ Cura.MachineAction
visible: Cura.MachineManager.hasMaterials
sourceComponent: numericTextFieldWithUnit
property string settingKey: "material_diameter"
property string label: catalog.i18nc("@label", "Material diameter")
property string label: catalog.i18nc("@label", "Compatible material diameter")
property string unit: catalog.i18nc("@label", "mm")
property string tooltip: catalog.i18nc("@tooltip", "The nominal diameter of filament supported by the printer. The exact diameter will be overridden by the material and/or the profile.")
function afterOnEditingFinished()
@ -441,7 +447,7 @@ Cura.MachineAction
width: settingsTabs.columnWidth
Label
{
text: catalog.i18nc("@label", "Extruder Start Gcode")
text: catalog.i18nc("@label", "Extruder Start G-code")
font.bold: true
}
Loader
@ -459,7 +465,7 @@ Cura.MachineAction
width: settingsTabs.columnWidth
Label
{
text: catalog.i18nc("@label", "Extruder End Gcode")
text: catalog.i18nc("@label", "Extruder End G-code")
font.bold: true
}
Loader
@ -591,7 +597,7 @@ Cura.MachineAction
const value = propertyProvider.properties.value;
return value ? value : "";
}
validator: RegExpValidator { regExp: _allowNegative ? /-?[0-9\.]{0,6}/ : /[0-9\.]{0,6}/ }
validator: RegExpValidator { regExp: _allowNegative ? /-?[0-9\.,]{0,6}/ : /[0-9\.,]{0,6}/ }
onEditingFinished:
{
if (propertyProvider && text != propertyProvider.properties.value)
@ -826,10 +832,10 @@ Cura.MachineAction
printHeadPolygon[axis][side] = result;
return result;
}
validator: RegExpValidator { regExp: /[0-9\.]{0,6}/ }
validator: RegExpValidator { regExp: /[0-9\.,]{0,6}/ }
onEditingFinished:
{
printHeadPolygon[axis][side] = parseFloat(textField.text);
printHeadPolygon[axis][side] = parseFloat(textField.text.replace(',','.'));
var polygon = [];
polygon.push([-printHeadPolygon["x"]["min"], printHeadPolygon["y"]["max"]]);
polygon.push([-printHeadPolygon["x"]["min"],-printHeadPolygon["y"]["min"]]);
@ -888,4 +894,4 @@ Cura.MachineAction
watchedProperties: [ "value" ]
storeIndex: manager.containerIndex
}
}
}

View file

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

View file

@ -246,7 +246,7 @@ Item {
Button
{
width: Math.floor(UM.Theme.getSize("setting").height / 2)
width: Math.round(UM.Theme.getSize("setting").height / 2)
height: UM.Theme.getSize("setting").height
onClicked: addedSettingsModel.setVisible(model.key, false)

View file

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

View file

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

View file

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

View file

@ -54,16 +54,15 @@ class PostProcessingPlugin(QObject, Extension):
## Execute all post-processing scripts on the gcode.
def execute(self, output_device):
scene = Application.getInstance().getController().getScene()
gcode_dict = None
if hasattr(scene, "gcode_dict"):
gcode_dict = getattr(scene, "gcode_dict")
# If the scene does not have a gcode, do nothing
if not hasattr(scene, "gcode_dict"):
return
gcode_dict = getattr(scene, "gcode_dict")
if not gcode_dict:
return
# get gcode list for the active build plate
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
return
@ -119,27 +118,30 @@ class PostProcessingPlugin(QObject, Extension):
for loader, script_name, ispkg in scripts:
# Iterate over all scripts.
if script_name not in sys.modules:
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py"))
loaded_script = importlib.util.module_from_spec(spec)
spec.loader.exec_module(loaded_script)
sys.modules[script_name] = loaded_script
loaded_class = getattr(loaded_script, script_name)
temp_object = loaded_class()
Logger.log("d", "Begin loading of script: %s", script_name)
try:
setting_data = temp_object.getSettingData()
if "name" in setting_data and "key" in setting_data:
self._script_labels[setting_data["key"]] = setting_data["name"]
self._loaded_scripts[setting_data["key"]] = loaded_class
else:
Logger.log("w", "Script %s.py has no name or key", script_name)
self._script_labels[script_name] = script_name
self._loaded_scripts[script_name] = loaded_class
except AttributeError:
Logger.log("e", "Script %s.py is not a recognised script type. Ensure it inherits Script", script_name)
except NotImplementedError:
Logger.log("e", "Script %s.py has no implemented settings", script_name)
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py"))
loaded_script = importlib.util.module_from_spec(spec)
spec.loader.exec_module(loaded_script)
sys.modules[script_name] = loaded_script
loaded_class = getattr(loaded_script, script_name)
temp_object = loaded_class()
Logger.log("d", "Begin loading of script: %s", script_name)
try:
setting_data = temp_object.getSettingData()
if "name" in setting_data and "key" in setting_data:
self._script_labels[setting_data["key"]] = setting_data["name"]
self._loaded_scripts[setting_data["key"]] = loaded_class
else:
Logger.log("w", "Script %s.py has no name or key", script_name)
self._script_labels[script_name] = script_name
self._loaded_scripts[script_name] = loaded_class
except AttributeError:
Logger.log("e", "Script %s.py is not a recognised script type. Ensure it inherits Script", script_name)
except NotImplementedError:
Logger.log("e", "Script %s.py has no implemented settings", script_name)
except Exception as e:
Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e)))
self.loadedScriptListChanged.emit()
loadedScriptListChanged = pyqtSignal()
@ -171,19 +173,19 @@ 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)]:
try:
path = os.path.join(root, "scripts")
if not os.path.isdir(path):
try:
os.makedirs(path)
except OSError:
Logger.log("w", "Unable to create a folder for scripts: " + path)
continue
# The PostProcessingPlugin path is for built-in scripts.
# The Resources path is where the user should store custom scripts.
# The Preferences path is legacy, where the user may previously have stored scripts.
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Resources), Resources.getStoragePath(Resources.Preferences)]:
path = os.path.join(root, "scripts")
if not os.path.isdir(path):
try:
os.makedirs(path)
except OSError:
Logger.log("w", "Unable to create a folder for scripts: " + path)
continue
self.loadAllScripts(path)
except Exception as e:
Logger.logException("e", "Exception occurred while loading post processing plugin: {error_msg}".format(error_msg = str(e)))
self.loadAllScripts(path)
# Create the plugin dialog component
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")

View file

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

View file

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

View file

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

View file

@ -2,17 +2,15 @@
# under the terms of the AGPLv3 or higher
from ..Script import Script
#from UM.Logger import Logger
# from cura.Settings.ExtruderManager import ExtruderManager
class ColorChange(Script):
class FilamentChange(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name":"Color Change",
"key": "ColorChange",
"name":"Filament Change",
"key": "FilamentChange",
"metadata": {},
"version": 2,
"settings":
@ -60,17 +58,17 @@ class ColorChange(Script):
if later_retract is not None and later_retract > 0.:
color_change = color_change + (" L%.2f" % later_retract)
color_change = color_change + " ; Generated by ColorChange plugin"
color_change = color_change + " ; Generated by FilamentChange plugin"
layer_targets = layer_nums.split(',')
layer_targets = layer_nums.split(",")
if len(layer_targets) > 0:
for layer_num in layer_targets:
layer_num = int( layer_num.strip() )
layer_num = int(layer_num.strip())
if layer_num < len(data):
layer = data[ layer_num - 1 ]
layer = data[layer_num - 1]
lines = layer.split("\n")
lines.insert(2, color_change )
final_line = "\n".join( lines )
data[ layer_num - 1 ] = final_line
lines.insert(2, color_change)
final_line = "\n".join(lines)
data[layer_num - 1] = final_line
return data

View file

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

View file

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

View file

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

View file

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

View file

@ -98,11 +98,14 @@ class SimulationView(View):
self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
self._compatibility_mode = True # for safety
self._compatibility_mode = self._evaluateCompatibilityMode()
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"),
title = catalog.i18nc("@info:title", "Simulation View"))
def _evaluateCompatibilityMode(self):
return OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
def _resetSettings(self):
self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
self._extruder_count = 0
@ -127,7 +130,7 @@ class SimulationView(View):
# Currently the RenderPass constructor requires a size > 0
# This should be fixed in RenderPass's constructor.
self._layer_pass = SimulationPass(1, 1)
self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
self._compatibility_mode = self._evaluateCompatibilityMode()
self._layer_pass.setSimulationView(self)
return self._layer_pass
@ -155,9 +158,10 @@ 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 not None:
self.setActivity(False)
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)
def isBusy(self):
return self._busy
@ -339,6 +343,11 @@ class SimulationView(View):
min_layer_number = sys.maxsize
max_layer_number = -sys.maxsize
for layer_id in layer_data.getLayers():
# If a layer doesn't contain any polygons, skip it (for infill meshes taller than print objects
if len(layer_data.getLayer(layer_id).polygons) < 1:
continue
# Store the max and min feedrates and thicknesses for display purposes
for p in layer_data.getLayer(layer_id).polygons:
self._max_feedrate = max(float(p.lineFeedrates.max()), self._max_feedrate)
@ -534,8 +543,7 @@ class SimulationView(View):
def _updateWithPreferences(self):
self._solid_layers = int(Preferences.getInstance().getValue("view/top_layer_count"))
self._only_show_top_layers = bool(Preferences.getInstance().getValue("view/only_show_top_layers"))
self._compatibility_mode = OpenGLContext.isLegacyOpenGL() or bool(
Preferences.getInstance().getValue("view/force_layer_view_compatibility_mode"))
self._compatibility_mode = self._evaluateCompatibilityMode()
self.setSimulationViewType(int(float(Preferences.getInstance().getValue("layerview/layer_view_type"))));
@ -632,4 +640,3 @@ class _CreateTopLayersJob(Job):
def cancel(self):
self._cancel = True
super().cancel()

View file

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

View file

@ -54,9 +54,13 @@ vertex41core =
vec4 layerThicknessGradientColor(float abs_value, float min_value, float max_value)
{
float value = (abs_value - min_value)/(max_value - min_value);
float red = max(2*value-1, 0);
float green = 1-abs(1-2*value);
float blue = max(1-2*value, 0);
float red = min(max(4*value-2, 0), 1);
float green = min(1.5*value, 0.75);
if (value > 0.75)
{
green = value;
}
float blue = 0.75-abs(0.25-value);
return vec4(red, green, blue, 1.0);
}

View file

@ -107,7 +107,7 @@ class SliceInfo(Extension):
"brand": extruder.material.getMetaData().get("brand", "")
}
extruder_position = int(extruder.getMetaDataEntry("position", "0"))
if extruder_position in print_information.materialLengths:
if len(print_information.materialLengths) > extruder_position:
extruder_dict["material_used"] = print_information.materialLengths[extruder_position]
extruder_dict["variant"] = extruder.variant.getName()
extruder_dict["nozzle_size"] = extruder.getProperty("machine_nozzle_size", "value")

View file

@ -78,17 +78,13 @@ 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:

View file

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

View file

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

View file

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

View file

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

After

Width:  |  Height:  |  Size: 866 B

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

View file

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

View file

@ -22,7 +22,7 @@ from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
from time import time
from datetime import datetime
from typing import Optional
from typing import Optional, Dict, List
import json
import os
@ -77,11 +77,13 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._cluster_size = int(properties.get(b"cluster_size", 0))
self._latest_reply_handler = None
def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs):
self.writeStarted.emit(self)
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", [])
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
@ -90,13 +92,15 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._gcode = gcode_list
is_job_sent = True
if len(self._printers) > 1:
self._spawnPrinterSelectionDialog()
else:
self.sendPrintJob()
is_job_sent = self.sendPrintJob()
# 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:
@ -111,14 +115,14 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
@pyqtSlot()
@pyqtSlot(str)
def sendPrintJob(self, target_printer = ""):
def sendPrintJob(self, target_printer: str = ""):
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
return False
self._sending_gcode = True
@ -131,7 +135,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
compressed_gcode = self._compressGCode()
if compressed_gcode is None:
# Abort was called.
return
return False
parts = []
@ -147,14 +151,16 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, compressed_gcode))
self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)
self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress)
return True
@pyqtProperty(QObject, notify=activePrinterChanged)
def activePrinter(self) -> Optional["PrinterOutputModel"]:
def activePrinter(self) -> Optional[PrinterOutputModel]:
return self._active_printer
@pyqtSlot(QObject)
def setActivePrinter(self, printer):
def setActivePrinter(self, printer: Optional[PrinterOutputModel]):
if self._active_printer != printer:
if self._active_printer and self._active_printer.camera:
self._active_printer.camera.stop()
@ -166,7 +172,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
@ -179,7 +185,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()
@ -187,30 +193,37 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._sending_gcode = False
Application.getInstance().getController().setActiveStage("PrepareStage")
# After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request
# the "reply" should be disconnected
if self._latest_reply_handler:
self._latest_reply_handler.disconnect()
self._latest_reply_handler = None
@pyqtSlot()
def openPrintJobControlPanel(self):
def openPrintJobControlPanel(self) -> None:
Logger.log("d", "Opening print job control panel...")
QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
@pyqtSlot()
def openPrinterControlPanel(self):
def openPrinterControlPanel(self) -> None:
Logger.log("d", "Opening printer control panel...")
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
@pyqtProperty("QVariantList", notify=printJobsChanged)
def printJobs(self):
def printJobs(self)-> List[PrintJobOutputModel] :
return self._print_jobs
@pyqtProperty("QVariantList", notify=printJobsChanged)
def queuedPrintJobs(self):
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is None]
def queuedPrintJobs(self) -> List[PrintJobOutputModel]:
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is None or print_job.state == "queued"]
@pyqtProperty("QVariantList", notify=printJobsChanged)
def activePrintJobs(self):
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None]
def activePrintJobs(self) -> List[PrintJobOutputModel]:
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"]
@pyqtProperty("QVariantList", notify=clusterPrintersChanged)
def connectedPrintersTypeCount(self):
def connectedPrintersTypeCount(self) -> List[PrinterOutputModel]:
printer_count = {}
for printer in self._printers:
if printer.type in printer_count:
@ -223,22 +236,22 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
return result
@pyqtSlot(int, result=str)
def formatDuration(self, seconds):
def formatDuration(self, seconds: int) -> str:
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
@pyqtSlot(int, result=str)
def getTimeCompleted(self, time_remaining):
def getTimeCompleted(self, time_remaining: int) -> str:
current_time = time()
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)
@pyqtSlot(int, result=str)
def getDateCompleted(self, time_remaining):
def getDateCompleted(self, time_remaining: int) -> str:
current_time = time()
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
def _printJobStateChanged(self):
def _printJobStateChanged(self) -> None:
username = self._getUserName()
if username is None:
@ -261,13 +274,13 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# Keep a list of all completed jobs so we know if something changed next time.
self._finished_jobs = finished_jobs
def _update(self):
def _update(self) -> None:
if not super()._update():
return
self.get("printers/", onFinished=self._onGetPrintersDataFinished)
self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished)
def _onGetPrintJobsFinished(self, reply: QNetworkReply):
def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
if not checkValidGetReply(reply):
return
@ -287,7 +300,11 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._updatePrintJob(print_job, print_job_data)
if print_job.state != "queued": # Print job should be assigned to a printer.
printer = self._getPrinterByKey(print_job_data["printer_uuid"])
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"])
else: # The job can "reserve" a printer if some changes are required.
printer = self._getPrinterByKey(print_job_data["assigned_to"])
@ -305,7 +322,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
@ -334,34 +351,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:
@ -392,13 +420,13 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
color = material_data["color"]
brand = material_data["brand"]
material_type = material_data["material"]
name = "Unknown"
name = "Empty" if material_data["material"] == "empty" else "Unknown"
material = MaterialOutputModel(guid=material_data["guid"], type=material_type,
brand=brand, color=color, name=name)
extruder.updateActiveMaterial(material)
def _removeJob(self, job):
def _removeJob(self, job: PrintJobOutputModel):
if job not in self._print_jobs:
return False
@ -409,7 +437,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
return True
def _removePrinter(self, printer):
def _removePrinter(self, printer: PrinterOutputModel):
self._printers.remove(printer)
if self._active_printer == printer:
self._active_printer = None

View file

@ -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)

View file

@ -32,10 +32,12 @@ Cura.MachineAction
if(base.selectedDevice && base.completeProperties)
{
var printerKey = base.selectedDevice.key
var printerName = base.selectedDevice.name // TODO To change when the groups have a name
if(manager.getStoredKey() != printerKey)
{
manager.setKey(printerKey);
completed();
manager.setKey(printerKey)
manager.setGroupName(printerName) // TODO To change when the groups have a name
completed()
}
}
}
@ -114,7 +116,7 @@ Cura.MachineAction
Column
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
spacing: UM.Theme.getSize("default_margin").height
ScrollView
@ -198,7 +200,7 @@ Cura.MachineAction
}
Column
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
visible: base.selectedDevice ? true : false
spacing: UM.Theme.getSize("default_margin").height
Label
@ -216,13 +218,13 @@ Cura.MachineAction
columns: 2
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Type")
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text:
{
@ -247,25 +249,25 @@ Cura.MachineAction
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Firmware version")
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: base.selectedDevice ? base.selectedDevice.firmwareVersion : ""
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Address")
}
Label
{
width: Math.floor(parent.width * 0.5)
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
text: base.selectedDevice ? base.selectedDevice.ipAddress : ""
}
@ -303,7 +305,7 @@ Cura.MachineAction
Button
{
text: catalog.i18nc("@action:button", "Connect")
enabled: (base.selectedDevice && base.completeProperties) ? true : false
enabled: (base.selectedDevice && base.completeProperties && base.selectedDevice.clusterSize > 0) ? true : false
onClicked: connectToPrinter()
}
}

View file

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

View file

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

View file

@ -78,7 +78,7 @@ Rectangle
Rectangle
{
width: Math.floor(parent.width / 3)
width: Math.round(parent.width / 3)
height: parent.height
Label // Print job name
@ -123,7 +123,7 @@ Rectangle
Rectangle
{
width: Math.floor(parent.width / 3 * 2)
width: Math.round(parent.width / 3 * 2)
height: parent.height
Label // Friendly machine name
@ -131,7 +131,7 @@ Rectangle
id: printerNameLabel
anchors.top: parent.top
anchors.left: parent.left
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width)
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width)
text: printer.name
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
@ -141,7 +141,7 @@ Rectangle
{
id: printerTypeLabel
anchors.top: printerNameLabel.bottom
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
text: printer.type
anchors.left: parent.left
elide: Text.ElideRight
@ -175,7 +175,7 @@ Rectangle
id: extruderInfo
anchors.bottom: parent.bottom
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
@ -183,7 +183,7 @@ Rectangle
PrintCoreConfiguration
{
id: leftExtruderInfo
width: Math.floor((parent.width - extruderSeperator.width) / 2)
width: Math.round((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.extruders[0]
}
@ -198,7 +198,7 @@ Rectangle
PrintCoreConfiguration
{
id: rightExtruderInfo
width: Math.floor((parent.width - extruderSeperator.width) / 2)
width: Math.round((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.extruders[1]
}
}
@ -209,7 +209,7 @@ Rectangle
anchors.right: parent.right
anchors.top: parent.top
height: showExtended ? parent.height: printProgressTitleBar.height
width: Math.floor(parent.width / 2 - UM.Theme.getSize("default_margin").width)
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
border.width: UM.Theme.getSize("default_lining").width
border.color: lineColor
radius: cornerRadius
@ -264,6 +264,7 @@ Rectangle
case "wait_for_configuration":
return catalog.i18nc("@label:status", "Reserved")
case "wait_cleanup":
case "wait_user_action":
return catalog.i18nc("@label:status", "Finished")
case "pre_print":
case "sent_to_printer":
@ -278,6 +279,7 @@ Rectangle
case "aborted":
return catalog.i18nc("@label:status", "Print aborted");
default:
// If print job has unknown status show printer.status
return printerStatusText(printer);
}
}
@ -413,7 +415,7 @@ Rectangle
{
if(printJob.state == "printing" || printJob.state == "post_print")
{
return OutputDevice.getDateCompleted(printJob.time_total - printJob.time_elapsed)
return OutputDevice.getDateCompleted(printJob.timeTotal - printJob.timeElapsed)
}
}
return "";

View file

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

View file

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

View file

@ -126,7 +126,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
def removeManualDevice(self, key, address = None):
if key in self._discovered_devices:
if not address:
address = self._printers[key].ipAddress
address = self._discovered_devices[key].ipAddress
self._onRemoveDevice(key)
if address in self._manual_instances:

View file

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

View file

@ -10,7 +10,7 @@ if MYPY:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
class USBPrinterOuptutController(PrinterOutputController):
class USBPrinterOutputController(PrinterOutputController):
def __init__(self, output_device):
super().__init__(output_device)

View file

@ -1,4 +1,4 @@
# Copyright (c) 2016 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
@ -12,12 +12,12 @@ from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from .AutoDetectBaudJob import AutoDetectBaudJob
from .USBPrinterOutputController import USBPrinterOuptutController
from .USBPrinterOutputController import USBPrinterOutputController
from .avr_isp import stk500v2, intelHex
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty
from serial import Serial, SerialException
from serial import Serial, SerialException, SerialTimeoutException
from threading import Thread
from time import time, sleep
from queue import Queue
@ -99,7 +99,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
Application.getInstance().getController().setActiveStage("MonitorStage")
# find the G-code for the active build plate to print
active_build_plate_id = Application.getInstance().getBuildPlateModel().activeBuildPlate
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict")
gcode_list = gcode_dict[active_build_plate_id]
@ -116,7 +116,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
@pyqtSlot(str)
def updateFirmware(self, file):
self._firmware_location = file
# the file path is qurl encoded.
self._firmware_location = file.replace("file://", "")
self.showFirmwareInterface()
self.setFirmwareUpdateState(FirmwareUpdateState.updating)
self._update_firmware_thread.start()
@ -126,9 +127,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if self._connection_state != ConnectionState.closed:
self.close()
hex_file = intelHex.readHex(self._firmware_location)
if len(hex_file) == 0:
Logger.log("e", "Unable to read provided hex file. Could not update firmware")
try:
hex_file = intelHex.readHex(self._firmware_location)
assert len(hex_file) > 0
except (FileNotFoundError, AssertionError):
Logger.log("e", "Unable to read provided hex file. Could not update firmware.")
self.setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error)
return
@ -198,7 +201,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
# Reset line number. If this is not done, first line is sometimes ignored
self._gcode.insert(0, "M110")
self._gcode_position = 0
self._is_printing = True
self._print_start_time = time()
self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds))
@ -206,6 +208,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
for i in range(0, 4): # Push first 4 entries before accepting other inputs
self._sendNextGcodeLine()
self._is_printing = True
self.writeFinished.emit(self)
def _autoDetectFinished(self, job: AutoDetectBaudJob):
@ -237,7 +240,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
container_stack = Application.getInstance().getGlobalContainerStack()
num_extruders = container_stack.getProperty("machine_extruder_count", "value")
# Ensure that a printer is created.
self._printers = [PrinterOutputModel(output_controller=USBPrinterOuptutController(self), number_of_extruders=num_extruders)]
self._printers = [PrinterOutputModel(output_controller=USBPrinterOutputController(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 = USBPrinterOuptutController(self), name= Application.getInstance().getPrintInformation().jobName)
print_job = PrintJobOutputModel(output_controller = USBPrinterOutputController(self), name= Application.getInstance().getPrintInformation().jobName)
print_job.updateState("printing")
self._printers[0].updateActivePrintJob(print_job)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,14 +3,9 @@
import configparser #To parse preference files.
import io #To serialise the preference files afterwards.
import os
from urllib.parse import quote_plus
from UM.Resources import Resources
from UM.VersionUpgrade import VersionUpgrade #We're inheriting from this.
from cura.CuraApplication import CuraApplication
# a list of all legacy "Not Supported" quality profiles
_OLD_NOT_SUPPORTED_PROFILES = [
@ -59,6 +54,12 @@ _EMPTY_CONTAINER_DICT = {
}
# Renamed definition files
_RENAMED_DEFINITION_DICT = {
"jellybox": "imade3d_jellybox",
}
class VersionUpgrade30to31(VersionUpgrade):
## Gets the version number from a CFG file in Uranium's 3.0 format.
#
@ -111,16 +112,9 @@ class VersionUpgrade30to31(VersionUpgrade):
if not parser.has_section(each_section):
parser.add_section(each_section)
# Copy global quality changes to extruder quality changes for single extrusion machines
if parser["metadata"]["type"] == "quality_changes":
all_quality_changes = self._getSingleExtrusionMachineQualityChanges(parser)
# Note that DO NOT!!! use the quality_changes returned from _getSingleExtrusionMachineQualityChanges().
# Those are loaded from the hard drive which are original files that haven't been upgraded yet.
# NOTE 2: The number can be 0 or 1 depends on whether you are loading it from the qualities folder or
# from a project file. When you load from a project file, the custom profile may not be in cura
# yet, so you will get 0.
if len(all_quality_changes) <= 1 and not parser.has_option("metadata", "extruder"):
self._createExtruderQualityChangesForSingleExtrusionMachine(filename, parser)
# Check renamed definitions
if "definition" in parser["general"] and parser["general"]["definition"] in _RENAMED_DEFINITION_DICT:
parser["general"]["definition"] = _RENAMED_DEFINITION_DICT[parser["general"]["definition"]]
# Update version numbers
parser["general"]["version"] = "2"
@ -131,7 +125,6 @@ class VersionUpgrade30to31(VersionUpgrade):
parser.write(output)
return [filename], [output.getvalue()]
## Upgrades a container stack from version 3.0 to 3.1.
#
# \param serialised The serialised form of a container stack.
@ -156,6 +149,10 @@ class VersionUpgrade30to31(VersionUpgrade):
if parser.has_option("containers", key) and parser["containers"][key] == "empty":
parser["containers"][key] = specific_empty_container
# check renamed definition
if parser.has_option("containers", "6") and parser["containers"]["6"] in _RENAMED_DEFINITION_DICT:
parser["containers"]["6"] = _RENAMED_DEFINITION_DICT[parser["containers"]["6"]]
# Update version numbers
if "general" not in parser:
parser["general"] = {}
@ -169,67 +166,3 @@ class VersionUpgrade30to31(VersionUpgrade):
output = io.StringIO()
parser.write(output)
return [filename], [output.getvalue()]
def _getSingleExtrusionMachineQualityChanges(self, quality_changes_container):
quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
quality_changes_containers = []
for item in os.listdir(quality_changes_dir):
file_path = os.path.join(quality_changes_dir, item)
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation = None)
try:
parser.read([file_path])
except:
# skip, it is not a valid stack file
continue
if not parser.has_option("metadata", "type"):
continue
if "quality_changes" != parser["metadata"]["type"]:
continue
if not parser.has_option("general", "name"):
continue
if quality_changes_container["general"]["name"] != parser["general"]["name"]:
continue
quality_changes_containers.append(parser)
return quality_changes_containers
def _createExtruderQualityChangesForSingleExtrusionMachine(self, filename, global_quality_changes):
suffix = "_" + quote_plus(global_quality_changes["general"]["name"].lower())
machine_name = os.path.os.path.basename(filename).replace(".inst.cfg", "").replace(suffix, "")
# Why is this here?!
# When we load a .curaprofile file the deserialize will trigger a version upgrade, creating a dangling file.
# This file can be recognized by it's lack of a machine name in the target filename.
# So when we detect that situation here, we don't create the file and return.
if machine_name == "":
return
new_filename = machine_name + "_" + "fdmextruder" + suffix
extruder_quality_changes_parser = configparser.ConfigParser(interpolation = None)
extruder_quality_changes_parser.add_section("general")
extruder_quality_changes_parser["general"]["version"] = str(2)
extruder_quality_changes_parser["general"]["name"] = global_quality_changes["general"]["name"]
extruder_quality_changes_parser["general"]["definition"] = global_quality_changes["general"]["definition"]
extruder_quality_changes_parser.add_section("metadata")
extruder_quality_changes_parser["metadata"]["quality_type"] = global_quality_changes["metadata"]["quality_type"]
extruder_quality_changes_parser["metadata"]["type"] = global_quality_changes["metadata"]["type"]
extruder_quality_changes_parser["metadata"]["setting_version"] = str(4)
extruder_quality_changes_parser["metadata"]["extruder"] = "fdmextruder"
extruder_quality_changes_output = io.StringIO()
extruder_quality_changes_parser.write(extruder_quality_changes_output)
extruder_quality_changes_filename = quote_plus(new_filename) + ".inst.cfg"
quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer)
with open(os.path.join(quality_changes_dir, extruder_quality_changes_filename), "w") as f:
f.write(extruder_quality_changes_output.getvalue())

View file

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

View file

@ -0,0 +1,135 @@
# 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"]
#Update version number.
parser["general"]["version"] = "3"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,8 @@ import UM.Dictionary
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.ContainerRegistry import ContainerRegistry
from .XmlMaterialValidator import XmlMaterialValidator
## Handles serializing and deserializing material containers from an XML file
class XmlMaterialProfile(InstanceContainer):
CurrentFdmMaterialVersion = "1.3"
@ -46,18 +48,35 @@ class XmlMaterialProfile(InstanceContainer):
## Overridden from InstanceContainer
# set the meta data for all machine / variant combinations
def setMetaDataEntry(self, key, value):
#
# The "apply_to_all" flag indicates whether this piece of metadata should be applied to all material containers
# or just this specific container.
# For example, when you change the material name, you want to apply it to all its derived containers, but for
# some specific settings, they should only be applied to a machine/variant-specific container.
#
def setMetaDataEntry(self, key, value, apply_to_all = True):
registry = ContainerRegistry.getInstance()
if registry.isReadOnly(self.getId()):
return
super().setMetaDataEntry(key, value)
# Prevent recursion
if not apply_to_all:
super().setMetaDataEntry(key, value)
return
basefile = self.getMetaDataEntry("base_file", self.getId()) #if basefile is self.getId, this is a basefile.
# Update all containers that share basefile
for container in registry.findInstanceContainers(base_file = basefile):
if container.getMetaDataEntry(key, None) != value: # Prevent recursion
container.setMetaDataEntry(key, value)
# Get the MaterialGroup
material_manager = CuraApplication.getInstance().getMaterialManager()
root_material_id = self.getMetaDataEntry("base_file") #if basefile is self.getId, this is a basefile.
material_group = material_manager.getMaterialGroup(root_material_id)
# Update the root material container
root_material_container = material_group.root_material_node.getContainer()
root_material_container.setMetaDataEntry(key, value, apply_to_all = False)
# Update all containers derived from it
for node in material_group.derived_material_node_list:
container = node.getContainer()
container.setMetaDataEntry(key, value, apply_to_all = False)
## Overridden from InstanceContainer, similar to setMetaDataEntry.
# without this function the setName would only set the name of the specific nozzle / material / machine combination container
@ -181,28 +200,39 @@ class XmlMaterialProfile(InstanceContainer):
## Begin Settings Block
builder.start("settings")
if self.getDefinition().getId() == "fdmprinter":
if self.getMetaDataEntry("definition") == "fdmprinter":
for instance in self.findInstances():
self._addSettingElement(builder, instance)
machine_container_map = {}
machine_nozzle_map = {}
machine_variant_map = {}
variant_manager = CuraApplication.getInstance().getVariantManager()
material_manager = CuraApplication.getInstance().getMaterialManager()
root_material_id = self.getMetaDataEntry("base_file") # if basefile is self.getId, this is a basefile.
material_group = material_manager.getMaterialGroup(root_material_id)
all_containers = []
for node in [material_group.root_material_node] + material_group.derived_material_node_list:
all_containers.append(node.getContainer())
all_containers = registry.findInstanceContainers(GUID = self.getMetaDataEntry("GUID"), base_file = self.getId())
for container in all_containers:
definition_id = container.getDefinition().getId()
definition_id = container.getMetaDataEntry("definition")
if definition_id == "fdmprinter":
continue
if definition_id not in machine_container_map:
machine_container_map[definition_id] = container
if definition_id not in machine_nozzle_map:
machine_nozzle_map[definition_id] = {}
if definition_id not in machine_variant_map:
machine_variant_map[definition_id] = {}
variant = container.getMetaDataEntry("variant")
if variant:
machine_nozzle_map[definition_id][variant] = container
variant_name = container.getMetaDataEntry("variant_name")
if variant_name:
variant_dict = {"variant_node": variant_manager.getVariantNode(definition_id, variant_name),
"material_container": container}
machine_variant_map[definition_id][variant_name] = variant_dict
continue
machine_container_map[definition_id] = container
@ -211,7 +241,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 = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = definition_id)[0]
product = definition_id
for product_name, product_id_list in product_id_map.items():
@ -221,45 +252,74 @@ class XmlMaterialProfile(InstanceContainer):
builder.start("machine")
builder.start("machine_identifier", {
"manufacturer": container.getMetaDataEntry("machine_manufacturer", definition.getMetaDataEntry("manufacturer", "Unknown")),
"manufacturer": container.getMetaDataEntry("machine_manufacturer",
definition_metadata.get("manufacturer", "Unknown")),
"product": product
})
builder.end("machine_identifier")
for instance in container.findInstances():
if self.getDefinition().getId() == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value:
if self.getMetaDataEntry("definition") == "fdmprinter" and self.getInstance(instance.definition.key) and self.getProperty(instance.definition.key, "value") == instance.value:
# If the settings match that of the base profile, just skip since we inherit the base profile.
continue
self._addSettingElement(builder, instance)
# Find all hotend sub-profiles corresponding to this material and machine and add them to this profile.
for hotend_id, hotend in machine_nozzle_map[definition_id].items():
variant_containers = registry.findInstanceContainersMetadata(id = hotend.getMetaDataEntry("variant"))
if not variant_containers:
continue
buildplate_dict = {}
for variant_name, variant_dict in machine_variant_map[definition_id].items():
variant_type = variant_dict["variant_node"].metadata["hardware_type"]
from cura.Machines.VariantManager import VariantType
variant_type = VariantType(variant_type)
if variant_type == VariantType.NOZZLE:
# The hotend identifier is not the containers name, but its "name".
builder.start("hotend", {"id": variant_name})
# The hotend identifier is not the containers name, but its "name".
builder.start("hotend", {"id": variant_containers[0]["name"]})
# Compatible is a special case, as it's added as a meta data entry (instead of an instance).
material_container = variant_dict["material_container"]
compatible = material_container.getMetaDataEntry("compatible")
if compatible is not None:
builder.start("setting", {"key": "hardware compatible"})
if compatible:
builder.data("yes")
else:
builder.data("no")
builder.end("setting")
# Compatible is a special case, as it's added as a meta data entry (instead of an instance).
compatible = hotend.getMetaDataEntry("compatible")
if compatible is not None:
builder.start("setting", {"key": "hardware compatible"})
if compatible:
builder.data("yes")
else:
builder.data("no")
builder.end("setting")
for instance in material_container.findInstances():
if container.getInstance(instance.definition.key) and container.getProperty(instance.definition.key, "value") == instance.value:
# If the settings match that of the machine profile, just skip since we inherit the machine profile.
continue
for instance in hotend.findInstances():
if container.getInstance(instance.definition.key) and container.getProperty(instance.definition.key, "value") == instance.value:
# If the settings match that of the machine profile, just skip since we inherit the machine profile.
continue
self._addSettingElement(builder, instance)
self._addSettingElement(builder, instance)
if material_container.getMetaDataEntry("buildplate_compatible") and not buildplate_dict:
buildplate_dict["buildplate_compatible"] = material_container.getMetaDataEntry("buildplate_compatible")
buildplate_dict["buildplate_recommended"] = material_container.getMetaDataEntry("buildplate_recommended")
buildplate_dict["material_container"] = material_container
builder.end("hotend")
builder.end("hotend")
if buildplate_dict:
for variant_name in buildplate_dict["buildplate_compatible"]:
builder.start("buildplate", {"id": variant_name})
material_container = buildplate_dict["material_container"]
buildplate_compatible_dict = material_container.getMetaDataEntry("buildplate_compatible")
buildplate_recommended_dict = material_container.getMetaDataEntry("buildplate_recommended")
if buildplate_compatible_dict:
compatible = buildplate_compatible_dict[variant_name]
recommended = buildplate_recommended_dict[variant_name]
builder.start("setting", {"key": "hardware compatible"})
builder.data("yes" if compatible else "no")
builder.end("setting")
builder.start("setting", {"key": "hardware recommended"})
builder.data("yes" if recommended else "no")
builder.end("setting")
builder.end("buildplate")
builder.end("machine")
@ -480,6 +540,10 @@ class XmlMaterialProfile(InstanceContainer):
if "adhesion_info" not in meta_data:
meta_data["adhesion_info"] = ""
validation_message = XmlMaterialValidator.validateMaterialMetaData(meta_data)
if validation_message is not None:
raise Exception("Not valid material profile: %s" % (validation_message))
property_values = {}
properties = data.iterfind("./um:properties/*", self.__namespaces)
for entry in properties:
@ -538,10 +602,8 @@ class XmlMaterialProfile(InstanceContainer):
for machine_id in machine_id_list:
definitions = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id)
if not definitions:
Logger.log("w", "No definition found for machine ID %s", machine_id)
continue
Logger.log("d", "Found definition for machine ID %s", machine_id)
definition = definitions[0]
machine_manufacturer = identifier.get("manufacturer", definition.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition.
@ -586,14 +648,11 @@ class XmlMaterialProfile(InstanceContainer):
if buildplate_id is None:
continue
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(
id = buildplate_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(
definition = machine_id, name = buildplate_id)
if not variant_containers:
from cura.Machines.VariantManager import VariantType
variant_manager = CuraApplication.getInstance().getVariantManager()
variant_node = variant_manager.getVariantNode(machine_id, buildplate_id,
variant_type = VariantType.BUILD_PLATE)
if not variant_node:
continue
buildplate_compatibility = machine_compatibility
@ -614,16 +673,14 @@ class XmlMaterialProfile(InstanceContainer):
hotends = machine.iterfind("./um:hotend", self.__namespaces)
for hotend in hotends:
hotend_id = hotend.get("id")
if hotend_id is None:
# The "id" field for hotends in material profiles are actually
hotend_name = hotend.get("id")
if hotend_name is None:
continue
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = hotend_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = hotend_id)
if not variant_containers:
variant_manager = CuraApplication.getInstance().getVariantManager()
variant_node = variant_manager.getVariantNode(machine_id, hotend_name)
if not variant_node:
continue
hotend_compatibility = machine_compatibility
@ -639,20 +696,20 @@ class XmlMaterialProfile(InstanceContainer):
else:
Logger.log("d", "Unsupported material setting %s", key)
new_hotend_id = self.getId() + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
new_hotend_specific_material_id = self.getId() + "_" + machine_id + "_" + hotend_name.replace(" ", "_")
# Same as machine compatibility, keep the derived material containers consistent with the parent material
if ContainerRegistry.getInstance().isLoaded(new_hotend_id):
new_hotend_material = ContainerRegistry.getInstance().findContainers(id = new_hotend_id)[0]
if ContainerRegistry.getInstance().isLoaded(new_hotend_specific_material_id):
new_hotend_material = ContainerRegistry.getInstance().findContainers(id = new_hotend_specific_material_id)[0]
is_new_material = False
else:
new_hotend_material = XmlMaterialProfile(new_hotend_id)
new_hotend_material = XmlMaterialProfile(new_hotend_specific_material_id)
is_new_material = True
new_hotend_material.setMetaData(copy.deepcopy(self.getMetaData()))
new_hotend_material.getMetaData()["id"] = new_hotend_id
new_hotend_material.getMetaData()["id"] = new_hotend_specific_material_id
new_hotend_material.getMetaData()["name"] = self.getName()
new_hotend_material.getMetaData()["variant"] = variant_containers[0]["id"]
new_hotend_material.getMetaData()["variant_name"] = hotend_name
new_hotend_material.setDefinition(machine_id)
# Don't use setMetadata, as that overrides it for all materials with same base file
new_hotend_material.getMetaData()["compatible"] = hotend_compatibility
@ -772,7 +829,6 @@ class XmlMaterialProfile(InstanceContainer):
for machine_id in machine_id_list:
definition_metadata = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id)
if not definition_metadata:
Logger.log("w", "No definition found for machine ID %s", machine_id)
continue
definition_metadata = definition_metadata[0]
@ -782,15 +838,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
@ -798,8 +850,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 = {}
@ -810,15 +861,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":
@ -826,50 +879,36 @@ class XmlMaterialProfile(InstanceContainer):
elif key == "hardware recommended":
buildplate_recommended = cls._parseCompatibleValue(entry.text)
buildplate_map["buildplate_compatible"][buildplate_id] = buildplate_map["buildplate_compatible"]
buildplate_map["buildplate_recommended"][buildplate_id] = buildplate_map["buildplate_recommended"]
buildplate_map["buildplate_compatible"][buildplate_id] = buildplate_compatibility
buildplate_map["buildplate_recommended"][buildplate_id] = buildplate_recommended
for hotend in machine.iterfind("./um:hotend", cls.__namespaces):
hotend_id = hotend.get("id")
if hotend_id is None:
hotend_name = hotend.get("id")
if hotend_name is None:
continue
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = hotend_id)
if not variant_containers:
# It is not really properly defined what "ID" is so also search for variants by name.
variant_containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(definition = machine_id, name = hotend_id)
hotend_compatibility = machine_compatibility
for entry in hotend.iterfind("./um:setting", cls.__namespaces):
key = entry.get("key")
if key == "hardware compatible":
hotend_compatibility = cls._parseCompatibleValue(entry.text)
new_hotend_id = container_id + "_" + machine_id + "_" + hotend_id.replace(" ", "_")
new_hotend_specific_material_id = container_id + "_" + machine_id + "_" + hotend_name.replace(" ", "_")
# Same as machine compatibility, keep the derived material containers consistent with the parent material
found_materials = ContainerRegistry.getInstance().findInstanceContainersMetadata(id = new_hotend_id)
if found_materials:
new_hotend_material_metadata = found_materials[0]
else:
new_hotend_material_metadata = {}
# Same as above, do not overwrite existing metadata.
new_hotend_material_metadata = {}
new_hotend_material_metadata.update(base_metadata)
if variant_containers:
new_hotend_material_metadata["variant"] = variant_containers[0]["id"]
else:
new_hotend_material_metadata["variant"] = hotend_id
_with_missing_variants.append(new_hotend_material_metadata)
new_hotend_material_metadata["variant_name"] = hotend_name
new_hotend_material_metadata["compatible"] = hotend_compatibility
new_hotend_material_metadata["machine_manufacturer"] = machine_manufacturer
new_hotend_material_metadata["id"] = new_hotend_id
new_hotend_material_metadata["id"] = new_hotend_specific_material_id
new_hotend_material_metadata["definition"] = machine_id
if buildplate_map["buildplate_compatible"]:
new_hotend_material_metadata["buildplate_compatible"] = buildplate_map["buildplate_compatible"]
new_hotend_material_metadata["buildplate_recommended"] = buildplate_map["buildplate_recommended"]
if len(found_materials) == 0:
result_metadata.append(new_hotend_material_metadata)
result_metadata.append(new_hotend_material_metadata)
# there is only one ID for a machine. Once we have reached here, it means we have already found
# a workable ID for that machine, so there is no need to continue
@ -911,11 +950,11 @@ class XmlMaterialProfile(InstanceContainer):
else:
merged_name_parts.append(part)
id_list = [name.lower().replace(" ", ""), # simply removing all spaces
id_list = {name.lower().replace(" ", ""), # simply removing all spaces
name.lower().replace(" ", "_"), # simply replacing all spaces with underscores
"_".join(merged_name_parts),
]
}
id_list = list(id_list)
return id_list
## Gets a mapping from product names in the XML files to their definition
@ -989,21 +1028,3 @@ def _indent(elem, level = 0):
# before the last }
def _tag_without_namespace(element):
return element.tag[element.tag.rfind("}") + 1:]
#While loading XML profiles, some of these profiles don't know what variant
#they belong to. We'd like to search by the machine ID and the variant's
#name, but we don't know the variant's ID. Not all variants have been loaded
#yet so we can't run a filter on the name and machine. The ID is unknown
#so we can't lazily load the variant either. So we have to wait until all
#the rest is loaded properly and then assign the correct variant to the
#material files that were missing it.
_with_missing_variants = []
def _fillMissingVariants():
registry = ContainerRegistry.getInstance()
for variant_metadata in _with_missing_variants:
variants = registry.findContainersMetadata(definition = variant_metadata["definition"], name = variant_metadata["variant"])
if not variants:
Logger.log("w", "Could not find variant for variant-specific material {material_id}.".format(material_id = variant_metadata["id"]))
continue
variant_metadata["variant"] = variants[0]["id"]
ContainerRegistry.allMetadataLoaded.connect(_fillMissingVariants)

View file

@ -0,0 +1,25 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
class XmlMaterialValidator():
@classmethod
def validateMaterialMetaData(cls, validation_metadata):
if validation_metadata.get("GUID") is None:
return "Missing GUID"
if validation_metadata.get("brand") is None:
return "Missing Brand"
if validation_metadata.get("material") is None:
return "Missing Material"
if validation_metadata.get("version") is None:
return "Missing Version"
return None