From e17fbb0db20e3a8a0b2e55f92b495856f87b0c62 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 22 Jan 2018 16:05:43 +0100 Subject: [PATCH 01/12] Remove copying 1st extruder value to all extruders as that breaks re-loading from files --- cura/Settings/ExtruderStack.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py index 51b27fea57..d6a72b72e4 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -62,11 +62,6 @@ class ExtruderStack(CuraContainerStack): # Only copy the value when this extruder doesn't have the value. if self.definitionChanges.hasProperty(key, "value"): - # If the first extruder has a value for this setting, we must copy it to the other extruders via the global stack. - # Note: this assumes the extruders are loaded in the same order as they are positioned on the machine. - if self.getMetaDataEntry("position") == "0": - setting_value = self.definitionChanges.getProperty(key, "value") - stack.definitionChanges.setProperty(key, "value", setting_value) continue setting_value = stack.definitionChanges.getProperty(key, "value") From 129f9cc16c33fcc3c4449f795c9a72bb389f1faf Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Mon, 22 Jan 2018 17:18:09 +0100 Subject: [PATCH 02/12] Fixes for custom FMD printer material diameter upgrade and storage - CURA-4835 --- cura/Settings/ExtruderManager.py | 84 +++++++++++++++++++ cura/Settings/ExtruderStack.py | 12 +++ .../MachineSettingsAction.py | 77 +---------------- 3 files changed, 97 insertions(+), 76 deletions(-) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 6d52ce87fd..6b52c629d0 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -502,6 +502,90 @@ class ExtruderManager(QObject): def getInstanceExtruderValues(self, key): return ExtruderManager.getExtruderValues(key) + ## Updates the material container to a material that matches the material diameter set for the printer + def updateMaterialForDiameter(self, extruder_position: int): + + global_stack = Application.getInstance().getGlobalContainerStack() + if not global_stack: + return + + if not global_stack.getMetaDataEntry("has_materials", False): + return + + extruder_stack = global_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 = global_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 global_stack.getMetaDataEntry("has_machine_materials", False): + materials_definition = global_stack.definition.getId() + has_material_variants = global_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() + + container_registry = Application.getInstance().getContainerRegistry() + empty_material = container_registry.findInstanceContainers(id = "empty_material")[0] + + if old_material == empty_material: + 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 = 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 = 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 = 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 = container_registry.findInstanceContainers(**search_criteria) + if not materials: + # Just use empty material as a final fallback + materials = [empty_material] + + Logger.log("i", "Selecting new material: %s", materials[0].getId()) + + extruder_stack.material = materials[0] + ## Get the value for a setting from a specific extruder. # # This is exposed to SettingFunction to use in value functions. diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py index d6a72b72e4..204b0e2dae 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -3,6 +3,7 @@ from typing import Any, TYPE_CHECKING, Optional +from UM.Application import Application from UM.Decorators import override from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase from UM.Settings.ContainerStack import ContainerStack @@ -60,6 +61,12 @@ class ExtruderStack(CuraContainerStack): for key in keys_to_copy: + # Since material_diameter is not on the extruder definition, we need to add it here + # WARNING: this might be very dangerous and should be refactored ASAP! + definition = stack.getSettingDefinition(key) + if definition: + self.definition.addDefinition(definition) + # Only copy the value when this extruder doesn't have the value. if self.definitionChanges.hasProperty(key, "value"): continue @@ -75,6 +82,11 @@ class ExtruderStack(CuraContainerStack): self.definitionChanges.addInstance(new_instance) self.definitionChanges.setDirty(True) + # Make sure the material diameter is up to date for the extruder stack. + if key == "material_diameter": + position = self.getMetaDataEntry("position", "0") + Application.getInstance().getExtruderManager().updateMaterialForDiameter(position) + # NOTE: We cannot remove the setting from the global stack's definition changes container because for # material diameter, it needs to be applied to all extruders, but here we don't know how many extruders # a machine actually has and how many extruders has already been loaded for that machine, so we have to diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index ae1c1663dd..baa0639d3f 100755 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -158,79 +158,4 @@ class MachineSettingsAction(MachineAction): @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] + Application.getInstance().getExtruderManager().updateMaterialForDiameter(extruder_position) From 3fb9877a30abb395ae9f854df6e71ff6d237ad83 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 22 Jan 2018 14:59:04 +0100 Subject: [PATCH 03/12] Add call_on_qt_thread to fix project loading crashing on rendering CURA-4839 See comments... --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 33 +++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 2c92b4cb6d..059a1c5d1f 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -31,10 +31,42 @@ import zipfile import io import configparser import os +import threading i18n_catalog = i18nCatalog("cura") +# +# HACK: +# +# In project loading, when override the existing machine is selected, the stacks and containers that are correctly +# active in the system will be overridden at runtime. Because the project loading is done in a different thread than +# the Qt thread, something else can kick in the middle of the process. One of them is the rendering. It will access +# the current stacks and container, which have not completely been updated yet, so Cura will crash in this case. +# +# This "@call_on_qt_thread" decorator makes sure that a function will always be called on the Qt thread (blocking). +# It is applied to the read() function of project loading so it can be guaranteed that only after the project loading +# process is completely done, everything else that needs to occupy the QT thread will be executed. +# +class InterCallObject: + def __init__(self): + self.finish_event = threading.Event() + self.result = None + + +def call_on_qt_thread(func): + def _call_on_qt_thread_wrapper(*args, **kwargs): + def _handle_call(ico, *args, **kwargs): + ico.result = func(*args, **kwargs) + ico.finish_event.set() + inter_call_object = InterCallObject() + new_args = tuple([inter_call_object] + list(args)[:]) + CuraApplication.getInstance().callLater(_handle_call, *new_args, **kwargs) + inter_call_object.finish_event.wait() + return inter_call_object.result + return _call_on_qt_thread_wrapper + + ## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): def __init__(self): @@ -401,6 +433,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # containing global.cfg / extruder.cfg # # \param file_name + @call_on_qt_thread def read(self, file_name): archive = zipfile.ZipFile(file_name, "r") From dc119d74bdea44fa8bc572a3da7003adc7c57b87 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 23 Jan 2018 09:21:38 +0100 Subject: [PATCH 04/12] CURA-4785 added logging for measuring time for validation check --- cura/Settings/MachineManager.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 8089cde876..1fcb42e824 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -1,6 +1,7 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +import time #Type hinting. from typing import Union, List, Dict @@ -390,6 +391,7 @@ class MachineManager(QObject): Logger.log("w", "Failed creating a new machine!") def _checkStacksHaveErrors(self) -> bool: + time_start = time.time() if self._global_container_stack is None: #No active machine. return False @@ -405,6 +407,7 @@ class MachineManager(QObject): if stack.hasErrors(): return True + Logger.log("d", "Checking stacks for errors took %.2f s" % (time.time() - time_start)) return False ## Remove all instances from the top instanceContainer (effectively removing all user-changed settings) From 92e99012d3050f22f34f09d349937aa0eb1c6ff2 Mon Sep 17 00:00:00 2001 From: Jack Ha Date: Tue, 23 Jan 2018 09:33:52 +0100 Subject: [PATCH 05/12] CURA-4785 now actually checking for extruder position, improved logs --- cura/Settings/MachineManager.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 1fcb42e824..e84f8d2237 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -396,18 +396,23 @@ class MachineManager(QObject): return False if self._global_container_stack.hasErrors(): + Logger.log("d", "Checking global stack for errors took %0.2f s and we found and error" % (time.time() - time_start)) return True # Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") extruder_stacks = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) - if len(extruder_stacks) > machine_extruder_count: - extruder_stacks = extruder_stacks[:machine_extruder_count] # we only have to check the used extruders + count = 1 # we start with the global stack for stack in extruder_stacks: + md = stack.getMetaData() + if "position" in md and int(md["position"]) >= machine_extruder_count: + continue + count += 1 if stack.hasErrors(): + Logger.log("d", "Checking %s stacks for errors took %.2f s and we found an error in stack [%s]" % (count, time.time() - time_start, str(stack))) return True - Logger.log("d", "Checking stacks for errors took %.2f s" % (time.time() - time_start)) + Logger.log("d", "Checking %s stacks for errors took %.2f s" % (count, time.time() - time_start)) return False ## Remove all instances from the top instanceContainer (effectively removing all user-changed settings) From 6c9d7b5a2ef29f0d022d09d66c216e85e38053c9 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 23 Jan 2018 09:49:26 +0100 Subject: [PATCH 06/12] Fixes for resetting print time information when removing all or last model, reduce signals being used for print time resetting - CURA-4852 --- cura/CuraActions.py | 4 ++++ cura/CuraApplication.py | 6 +++--- cura/PrintInformation.py | 31 +++++++++++++++++++++++-------- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/cura/CuraActions.py b/cura/CuraActions.py index f5aace805b..f517ec4217 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -94,6 +94,10 @@ class CuraActions(QObject): removed_group_nodes.append(group_node) op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent())) op.addOperation(RemoveSceneNodeOperation(group_node)) + + # Reset the print information + Application.getInstance().getController().getScene().sceneChanged.emit(node) + op.push() ## Set the extruder that should be used to print the selection. diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index bfcbe529f7..8df9f01e91 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -1066,12 +1066,12 @@ class CuraApplication(QtApplication): for node in nodes: op.addOperation(RemoveSceneNodeOperation(node)) + # Reset the print information + self.getController().getScene().sceneChanged.emit(node) + op.push() Selection.clear() - # Reset the print information: - self.getController().getScene().sceneChanged.emit(node) - ## Reset all translation on nodes with mesh data. @pyqtSlot() def resetAllTranslation(self): diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 5d5d59ed3b..c03cafe667 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -8,7 +8,9 @@ from UM.Application import Application from UM.Logger import Logger from UM.Qt.Duration import Duration from UM.Preferences import Preferences +from UM.Scene.SceneNode import SceneNode from UM.Settings.ContainerRegistry import ContainerRegistry +from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Settings.ExtruderManager import ExtruderManager from typing import Dict @@ -65,7 +67,7 @@ class PrintInformation(QObject): self._backend = Application.getInstance().getBackend() if self._backend: self._backend.printDurationMessage.connect(self._onPrintDurationMessage) - Application.getInstance().getController().getScene().sceneChanged.connect(self.setToZeroPrintInformation) + Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged) self._base_name = "" self._abbr_machine = "" @@ -395,12 +397,25 @@ class PrintInformation(QObject): return result # Simulate message with zero time duration - def setToZeroPrintInformation(self, build_plate_number): - temp_message = {} - if build_plate_number not in self._print_time_message_values: - self._print_time_message_values[build_plate_number] = {} - for key in self._print_time_message_values[build_plate_number].keys(): - temp_message[key] = 0 + def setToZeroPrintInformation(self, build_plate): + # Construct the 0-time message + temp_message = {} + if build_plate not in self._print_time_message_values: + self._print_time_message_values[build_plate] = {} + for key in self._print_time_message_values[build_plate].keys(): + temp_message[key] = 0 temp_material_amounts = [0] - self._onPrintDurationMessage(build_plate_number, temp_message, temp_material_amounts) + + self._onPrintDurationMessage(build_plate, temp_message, temp_material_amounts) + + ## Listen to scene changes to check if we need to reset the print information + def _onSceneChanged(self, scene_node): + + # Ignore any changes that are not related to sliceable objects + if not isinstance(scene_node, SceneNode)\ + or not scene_node.callDecoration("isSliceable")\ + or not scene_node.callDecoration("getBuildPlateNumber") == self._active_build_plate: + return + + self.setToZeroPrintInformation(self._active_build_plate) From c0bce6ffd49338f53f7edf82d2541982b882543a Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 23 Jan 2018 10:25:39 +0100 Subject: [PATCH 07/12] Move Jenkins timeout into parallel_nodes block --- Jenkinsfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 20c7303719..83104aea18 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,5 +1,5 @@ -timeout(time: 2, unit: "HOURS") { - parallel_nodes(['linux && cura', 'windows && cura']) { +parallel_nodes(['linux && cura', 'windows && cura']) { + timeout(time: 2, unit: "HOURS") { // Prepare building stage('Prepare') { // Ensure we start with a clean build directory. From 1c605a5108771add6f3fd180f5073f5c33391eab Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 23 Jan 2018 10:58:43 +0100 Subject: [PATCH 08/12] Fix different strategies for machine and quality in project loading CURA-4839 --- cura/Settings/CuraContainerRegistry.py | 38 +++++++++++++-------- cura/Settings/ExtruderManager.py | 1 - cura/Settings/ExtruderStack.py | 1 - plugins/3MFReader/ThreeMFWorkspaceReader.py | 20 +++++++++-- 4 files changed, 41 insertions(+), 19 deletions(-) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 7231fa1f72..5d81188750 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -449,7 +449,13 @@ class CuraContainerRegistry(ContainerRegistry): if not extruder_stacks: self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder") - def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id): + # + # new_global_quality_changes is optional. It is only used in project loading for a scenario like this: + # - override the current machine + # - create new for custom quality profile + # new_global_quality_changes is the new global quality changes container in this scenario. + # + def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None): new_extruder_id = extruder_id extruder_definitions = self.findDefinitionContainers(id = new_extruder_id) @@ -545,8 +551,12 @@ class CuraContainerRegistry(ContainerRegistry): quality_id = "empty_quality" extruder_stack.setQualityById(quality_id) - if machine.qualityChanges.getId() not in ("empty", "empty_quality_changes"): - extruder_quality_changes_container = self.findInstanceContainers(name = machine.qualityChanges.getName(), extruder = extruder_id) + machine_quality_changes = machine.qualityChanges + if new_global_quality_changes is not None: + machine_quality_changes = new_global_quality_changes + + if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): + extruder_quality_changes_container = self.findInstanceContainers(name = machine_quality_changes.getName(), extruder = extruder_id) if extruder_quality_changes_container: extruder_quality_changes_container = extruder_quality_changes_container[0] @@ -556,34 +566,34 @@ class CuraContainerRegistry(ContainerRegistry): # Some extruder quality_changes containers can be created at runtime as files in the qualities # folder. Those files won't be loaded in the registry immediately. So we also need to search # the folder to see if the quality_changes exists. - extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine.qualityChanges.getName()) + extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) if extruder_quality_changes_container: quality_changes_id = extruder_quality_changes_container.getId() extruder_stack.setQualityChangesById(quality_changes_id) else: # if we still cannot find a quality changes container for the extruder, create a new one - container_name = machine.qualityChanges.getName() + container_name = machine_quality_changes.getName() container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name) extruder_quality_changes_container = InstanceContainer(container_id) extruder_quality_changes_container.setName(container_name) extruder_quality_changes_container.addMetaDataEntry("type", "quality_changes") extruder_quality_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) extruder_quality_changes_container.addMetaDataEntry("extruder", extruder_stack.definition.getId()) - extruder_quality_changes_container.addMetaDataEntry("quality_type", machine.qualityChanges.getMetaDataEntry("quality_type")) - extruder_quality_changes_container.setDefinition(machine.qualityChanges.getDefinition().getId()) + extruder_quality_changes_container.addMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type")) + extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId()) self.addContainer(extruder_quality_changes_container) extruder_stack.qualityChanges = extruder_quality_changes_container if not extruder_quality_changes_container: Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", - machine.qualityChanges.getName(), extruder_stack.getId()) + machine_quality_changes.getName(), extruder_stack.getId()) else: # move all per-extruder settings to the extruder's quality changes - for qc_setting_key in machine.qualityChanges.getAllKeys(): + for qc_setting_key in machine_quality_changes.getAllKeys(): settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") if settable_per_extruder: - setting_value = machine.qualityChanges.getProperty(qc_setting_key, "value") + setting_value = machine_quality_changes.getProperty(qc_setting_key, "value") setting_definition = machine.getSettingDefinition(qc_setting_key) new_instance = SettingInstance(setting_definition, definition_changes) @@ -592,7 +602,7 @@ class CuraContainerRegistry(ContainerRegistry): extruder_quality_changes_container.addInstance(new_instance) extruder_quality_changes_container.setDirty(True) - machine.qualityChanges.removeInstance(qc_setting_key, postpone_emit=True) + machine_quality_changes.removeInstance(qc_setting_key, postpone_emit=True) else: extruder_stack.setQualityChangesById("empty_quality_changes") @@ -600,8 +610,8 @@ class CuraContainerRegistry(ContainerRegistry): # Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have # per-extruder settings in the container for the machine instead of the extruder. - if machine.qualityChanges.getId() not in ("empty", "empty_quality_changes"): - quality_changes_machine_definition_id = machine.qualityChanges.getDefinition().getId() + if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): + quality_changes_machine_definition_id = machine_quality_changes.getDefinition().getId() else: whole_machine_definition = machine.definition machine_entry = machine.definition.getMetaDataEntry("machine") @@ -621,7 +631,7 @@ class CuraContainerRegistry(ContainerRegistry): qc_groups[qc_name] = [] qc_groups[qc_name].append(qc) # try to find from the quality changes cura directory too - quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine.qualityChanges.getName()) + quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) if quality_changes_container: qc_groups[qc_name].append(quality_changes_container) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 6b52c629d0..f9f0fbb401 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -504,7 +504,6 @@ class ExtruderManager(QObject): ## Updates the material container to a material that matches the material diameter set for the printer def updateMaterialForDiameter(self, extruder_position: int): - global_stack = Application.getInstance().getGlobalContainerStack() if not global_stack: return diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py index 204b0e2dae..0854964898 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -60,7 +60,6 @@ class ExtruderStack(CuraContainerStack): keys_to_copy = ["material_diameter", "machine_nozzle_size"] # these will be copied over to all extruders for key in keys_to_copy: - # Since material_diameter is not on the extruder definition, we need to add it here # WARNING: this might be very dangerous and should be refactored ASAP! definition = stack.getSettingDefinition(key) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 059a1c5d1f..3267bf486f 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -558,6 +558,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)] user_instance_containers = [] quality_and_definition_changes_instance_containers = [] + quality_changes_instance_containers = [] for instance_container_file in instance_container_files: container_id = self._stripFileToId(instance_container_file) serialized = archive.open(instance_container_file).read().decode("utf-8") @@ -663,6 +664,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # The ID already exists, but nothing in the values changed, so do nothing. pass quality_and_definition_changes_instance_containers.append(instance_container) + if container_type == "quality_changes": + quality_changes_instance_containers.append(instance_container) if container_type == "definition_changes": definition_changes_extruder_count = instance_container.getProperty("machine_extruder_count", "value") @@ -787,7 +790,19 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # If not extruder stacks were saved in the project file (pre 3.1) create one manually # We re-use the container registry's addExtruderStackForSingleExtrusionMachine method for this if not extruder_stacks: - stack = self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder") + # If we choose to override a machine but to create a new custom quality profile, the custom quality + # profile is not immediately applied to the global_stack, so this fix for single extrusion machines + # will use the current custom quality profile on the existing machine. The extra optional argument + # in that function is used in thia case to specify a new global stack quality_changes container so + # the fix can correctly create and copy over the custom quality settings to the newly created extruder. + new_global_quality_changes = None + if self._resolve_strategies["quality_changes"] == "new" and len(quality_changes_instance_containers) > 0: + new_global_quality_changes = quality_changes_instance_containers[0] + stack = self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder", + new_global_quality_changes) + if new_global_quality_changes is not None: + quality_changes_instance_containers.append(stack.qualityChanges) + quality_and_definition_changes_instance_containers.append(stack.qualityChanges) if global_stack.quality.getId() in ("empty", "empty_quality"): stack.quality = empty_quality_container if self._resolve_strategies["machine"] == "override": @@ -1028,8 +1043,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): stack.setNextStack(global_stack) stack.containersChanged.emit(stack.getTop()) else: - if quality_has_been_changed: - CuraApplication.getInstance().getMachineManager().activeQualityChanged.emit() + CuraApplication.getInstance().getMachineManager().activeQualityChanged.emit() # Actually change the active machine. Application.getInstance().setGlobalContainerStack(global_stack) From 5c8d46b5c2ff27ff32bfc8e27451d3f8764accf8 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 22 Jan 2018 17:21:03 +0100 Subject: [PATCH 09/12] Simplify check for _outside_buildarea There is a getter function that has a default if the attribute doesn't exist. Contributes to issue CURA-4797. --- cura/PlatformPhysics.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 69890178e4..cf4dd83fef 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -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. from PyQt5.QtCore import QTimer @@ -57,7 +57,7 @@ class PlatformPhysics: nodes = list(BreadthFirstIterator(root)) # Only check nodes inside build area. - nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea)] + nodes = [node for node in nodes if getattr(node, "_outside_buildarea", False)] random.shuffle(nodes) for node in nodes: From cf556ccf8ff7116af0014ffc91c019b26cf4f364 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 23 Jan 2018 11:20:06 +0100 Subject: [PATCH 10/12] Remove unused import Contributes to issue CURA-4797. --- cura/Scene/CuraSceneNode.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index 9df2931f0b..597dfedf7a 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -1,5 +1,7 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + from UM.Application import Application -from UM.Logger import Logger from UM.Scene.SceneNode import SceneNode from copy import deepcopy From 27e441ecd9642530b5b8f8fe949cff152d54dcba Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 23 Jan 2018 11:21:32 +0100 Subject: [PATCH 11/12] Do boundary checks on nodes for which the boundary check is unknown Just before deciding whether to drop down the node on the build plate. Contributes to issue CURA-4797. --- cura/BuildVolume.py | 84 ++++++++++++++++++++--------------------- cura/PlatformPhysics.py | 11 ++++-- 2 files changed, 48 insertions(+), 47 deletions(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 2567641cc9..f7e748ba4e 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -1,8 +1,7 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from cura.Settings.ExtruderManager import ExtruderManager -from UM.Settings.ContainerRegistry import ContainerRegistry from UM.i18n import i18nCatalog from UM.Scene.Platform import Platform from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator @@ -194,52 +193,51 @@ class BuildVolume(SceneNode): return True - ## For every sliceable node, update node._outside_buildarea + ## For every sliceable node, update node._outside_buildarea. + def updateAllBoundaryChecks(self): + self.updateNodeBoundaryCheck(Application.getInstance().getController().getScene().getRoot()) + + ## For a single node, update _outside_buildarea. # - def updateNodeBoundaryCheck(self): - root = Application.getInstance().getController().getScene().getRoot() - nodes = list(BreadthFirstIterator(root)) - group_nodes = [] + # If the node is a group node, the child nodes will also get updated. + # \param node The node to update the boundary checks of. + def updateNodeBoundaryCheck(self, node: SceneNode): + if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): + for child in node.getChildren(): #Still update the children! For instance, the root is not sliceable. + self.updateNodeBoundaryCheck(child) + return #Don't compute for non-sliceable nodes. - build_volume_bounding_box = self.getBoundingBox() - if build_volume_bounding_box: - # It's over 9000! - build_volume_bounding_box = build_volume_bounding_box.set(bottom=-9001) - else: - # No bounding box. This is triggered when running Cura from command line with a model for the first time - # In that situation there is a model, but no machine (and therefore no build volume. + #Mark the node as outside the build volume if the bounding box test fails. + build_volume = self.getBoundingBox() + if build_volume is None: + #No bounding box. This is triggered when running Cura from command line with a model for the first time. + #In that situation there is a model, but no machine (and therefore no build volume). return + build_volume = build_volume.set(bottom = -999999) #Allow models to clip the build plate. This should allow printing but remove the bottom side of the model underneath the build plate. + bounding_box = node.getBoundingBox() + if build_volume.intersectsBox(bounding_box) != AxisAlignedBox.IntersectionResult.FullIntersection: + node._outside_buildarea = True + else: - for node in nodes: - # Need to check group nodes later - if node.callDecoration("isGroup"): - group_nodes.append(node) # Keep list of affected group_nodes - - if node.callDecoration("isSliceable") or node.callDecoration("isGroup"): - node._outside_buildarea = False - bbox = node.getBoundingBox() - - # Mark the node as outside the build volume if the bounding box test fails. - if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection: + #Check for collisions between disallowed areas and the object. + convex_hull = node.callDecoration("getConvexHull") + if not convex_hull or not convex_hull.isValid(): + return + for area in self.getDisallowedAreas(): + overlap = convex_hull.intersectsPolygon(area) + if overlap is not None: node._outside_buildarea = True - continue + break + else: + node._outside_buildarea = False - convex_hull = node.callDecoration("getConvexHull") - if convex_hull: - if not convex_hull.isValid(): - return - # Check for collisions between disallowed areas and the object - for area in self.getDisallowedAreas(): - overlap = convex_hull.intersectsPolygon(area) - if overlap is None: - continue - node._outside_buildarea = True - continue - - # Group nodes should override the _outside_buildarea property of their children. - for group_node in group_nodes: - for child_node in group_node.getAllChildren(): - child_node._outside_buildarea = group_node._outside_buildarea + #Group nodes should override the _outside_buildarea property of their children. + if node.callDecoration("isGroup"): + for child in node.getAllChildren(): + child._outside_buildarea = node._outside_buildarea + else: + for child in node.getChildren(): + self.updateNodeBoundaryCheck(child) ## Recalculates the build volume & disallowed areas. def rebuild(self): @@ -424,7 +422,7 @@ class BuildVolume(SceneNode): Application.getInstance().getController().getScene()._maximum_bounds = scale_to_max_bounds - self.updateNodeBoundaryCheck() + self.updateAllBoundaryChecks() def getBoundingBox(self) -> AxisAlignedBox: return self._volume_aabb diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index cf4dd83fef..05385d7c71 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -56,14 +56,17 @@ class PlatformPhysics: # By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve. nodes = list(BreadthFirstIterator(root)) - # Only check nodes inside build area. - nodes = [node for node in nodes if getattr(node, "_outside_buildarea", False)] - random.shuffle(nodes) for node in nodes: if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None: continue + #Only check nodes inside the build area. + if not hasattr(node, "_outside_buildarea"): + self._build_volume.updateNodeBoundaryCheck(node) + if getattr(node, "_outside_buildarea", True): + continue + bbox = node.getBoundingBox() # Move it downwards if bottom is above platform @@ -155,7 +158,7 @@ class PlatformPhysics: # After moving, we have to evaluate the boundary checks for nodes build_volume = Application.getInstance().getBuildVolume() - build_volume.updateNodeBoundaryCheck() + build_volume.updateAllBoundaryChecks() def _onToolOperationStarted(self, tool): self._enabled = False From a31d65786b0c8a0a62c1a3637db914d88bbcc6b4 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Tue, 23 Jan 2018 11:24:43 +0100 Subject: [PATCH 12/12] Rename Enable Travel Optimization to Infill Travel Optimization Because it's now no longer in the Infill category this is unclear. --- resources/definitions/fdmprinter.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 0451bd2539..60ede2c3b0 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -5308,7 +5308,7 @@ }, "infill_enable_travel_optimization": { - "label": "Enable Travel Optimization", + "label": "Infill Travel Optimization", "description": "When enabled, the order in which the infill lines are printed is optimized to reduce the distance travelled. The reduction in travel time achieved very much depends on the model being sliced, infill pattern, density, etc. Note that, for some models that have many small areas of infill, the time to slice the model may be greatly increased.", "type": "bool", "default_value": false,