diff --git a/.gitignore b/.gitignore index ac1e8eba92..f67add62cf 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ plugins/cura-god-mode-plugin plugins/cura-big-flame-graph plugins/cura-siemensnx-plugin plugins/CuraVariSlicePlugin +plugins/CuraLiveScriptingPlugin #Build stuff CMakeCache.txt diff --git a/README.md b/README.md index f8d809df16..ba6a986093 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ For crashes and similar issues, please attach the following information: If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder +For additional support, you could also ask in the #cura channel on FreeNode IRC. For help with development, there is also the #cura-dev channel. + Dependencies ------------ diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py index 3cced2f85c..f0c29f5b81 100644 --- a/cura/PrintInformation.py +++ b/cura/PrintInformation.py @@ -70,6 +70,7 @@ class PrintInformation(QObject): Application.getInstance().globalContainerStackChanged.connect(self._updateJobName) Application.getInstance().fileLoaded.connect(self.setBaseName) + Application.getInstance().workspaceLoaded.connect(self.setProjectName) Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged) self._active_material_container = None @@ -283,7 +284,11 @@ class PrintInformation(QObject): return self._base_name @pyqtSlot(str) - def setBaseName(self, base_name): + def setProjectName(self, name): + self.setBaseName(name, is_project_file = True) + + @pyqtSlot(str) + def setBaseName(self, base_name, is_project_file = False): # Ensure that we don't use entire path but only filename name = os.path.basename(base_name) @@ -291,15 +296,17 @@ class PrintInformation(QObject): # extension. This cuts the extension off if necessary. name = os.path.splitext(name)[0] + # if this is a profile file, always update the job name # name is "" when I first had some meshes and afterwards I deleted them so the naming should start again is_empty = name == "" - if is_empty or (self._base_name == "" and self._base_name != name): + if is_project_file or (is_empty or (self._base_name == "" and self._base_name != name)): # remove ".curaproject" suffix from (imported) the file name if name.endswith(".curaproject"): name = name[:name.rfind(".curaproject")] self._base_name = name self._updateJobName() + ## Created an acronymn-like abbreviated machine name from the currently active machine name # Called each time the global stack is switched def _setAbbreviatedMachineName(self): diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 7abe5b35f5..3590d70602 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -4,6 +4,7 @@ import os import os.path import re +import configparser from typing import Optional @@ -19,6 +20,7 @@ from UM.Message import Message from UM.Platform import Platform from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with. from UM.Util import parseBool +from UM.Resources import Resources from . import ExtruderStack from . import GlobalStack @@ -409,7 +411,7 @@ class CuraContainerRegistry(ContainerRegistry): extruder_stack = None # if extruders are defined in the machine definition use those instead - if machine.extruders and len(machine.extruders) > 0: + if machine.extruders and "0" in machine.extruders: new_extruder_id = machine.extruders["0"].getId() extruder_stack = machine.extruders["0"] @@ -444,20 +446,82 @@ class CuraContainerRegistry(ContainerRegistry): self.addContainer(user_container) variant_id = "default" - if machine.variant.getId() != "empty_variant": + if machine.variant.getId() not in ("empty", "empty_variant"): variant_id = machine.variant.getId() + else: + variant_id = "empty_variant" extruder_stack.setVariantById(variant_id) - extruder_stack.setMaterialById("default") - extruder_stack.setQualityById("default") - quality_changes_id = "default" - if machine.qualityChanges.getId() != "empty_quality_changes": + + material_id = "default" + if machine.material.getId() not in ("empty", "empty_material"): + material_id = machine.material.getId() + else: + material_id = "empty_material" + extruder_stack.setMaterialById(material_id) + + quality_id = "default" + if machine.quality.getId() not in ("empty", "empty_quality"): + quality_id = machine.quality.getId() + else: + 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) if extruder_quality_changes_container: - quality_changes_id = extruder_quality_changes_container[0].getId() - extruder_stack.setQualityChangesById(quality_changes_id) + extruder_quality_changes_container = extruder_quality_changes_container[0] + quality_changes_id = extruder_quality_changes_container.getId() + extruder_stack.setQualityChangesById(quality_changes_id) + else: + # 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()) + if extruder_quality_changes_container: + quality_changes_id = extruder_quality_changes_container.getId() + extruder_stack.setQualityChangesById(quality_changes_id) + + 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()) self.addContainer(extruder_stack) + return extruder_stack + + def _findQualityChangesContainerInCuraFolder(self, name): + quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityInstanceContainer) + + instance_container = None + + 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() + try: + parser.read([file_path]) + except: + # skip, it is not a valid stack file + continue + + if not parser.has_option("general", "name"): + continue + + if parser["general"]["name"] == name: + # load the container + container_id = os.path.basename(file_path).replace(".inst.cfg", "") + + instance_container = InstanceContainer(container_id) + with open(file_path, "r") as f: + serialized = f.read() + instance_container.deserialize(serialized, file_path) + self.addContainer(instance_container) + break + + return instance_container + # Fix the extruders that were upgraded to ExtruderStack instances during addContainer. # The stacks are now responsible for setting the next stack on deserialize. However, # due to problems with loading order, some stacks may not have the proper next stack diff --git a/cura/Settings/CuraContainerStack.py b/cura/Settings/CuraContainerStack.py index 2a804def4d..f0cec3c450 100755 --- a/cura/Settings/CuraContainerStack.py +++ b/cura/Settings/CuraContainerStack.py @@ -41,9 +41,20 @@ class CuraContainerStack(ContainerStack): def __init__(self, container_id: str, *args, **kwargs): super().__init__(container_id, *args, **kwargs) - self._empty_instance_container = ContainerRegistry.getInstance().getEmptyInstanceContainer() + self._container_registry = ContainerRegistry.getInstance() + + self._empty_instance_container = self._container_registry.getEmptyInstanceContainer() + + self._empty_quality_changes = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0] + self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0] + self._empty_material = self._container_registry.findInstanceContainers(id = "empty_material")[0] + self._empty_variant = self._container_registry.findInstanceContainers(id = "empty_variant")[0] self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] + self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes + self._containers[_ContainerIndexes.Quality] = self._empty_quality + self._containers[_ContainerIndexes.Material] = self._empty_material + self._containers[_ContainerIndexes.Variant] = self._empty_variant self.containersChanged.connect(self._onContainersChanged) @@ -110,7 +121,7 @@ class CuraContainerStack(ContainerStack): # # \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID. def setQualityById(self, new_quality_id: str) -> None: - quality = self._empty_instance_container + quality = self._empty_quality if new_quality_id == "default": new_quality = self.findDefaultQuality() if new_quality: @@ -148,7 +159,7 @@ class CuraContainerStack(ContainerStack): # # \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID. def setMaterialById(self, new_material_id: str) -> None: - material = self._empty_instance_container + material = self._empty_material if new_material_id == "default": new_material = self.findDefaultMaterial() if new_material: @@ -186,7 +197,7 @@ class CuraContainerStack(ContainerStack): # # \throws Exceptions.InvalidContainerError Raised when no container could be found with the specified ID. def setVariantById(self, new_variant_id: str) -> None: - variant = self._empty_instance_container + variant = self._empty_variant if new_variant_id == "default": new_variant = self.findDefaultVariant() if new_variant: @@ -348,8 +359,8 @@ class CuraContainerStack(ContainerStack): # # \throws InvalidContainerStackError Raised when no definition can be found for the stack. @override(ContainerStack) - def deserialize(self, contents: str) -> None: - super().deserialize(contents) + def deserialize(self, contents: str, file_name: Optional[str] = None) -> None: + super().deserialize(contents, file_name) new_containers = self._containers.copy() while len(new_containers) < len(_ContainerIndexes.IndexTypeMap): @@ -456,7 +467,7 @@ class CuraContainerStack(ContainerStack): else: search_criteria["definition"] = "fdmprinter" - if self.material != self._empty_instance_container: + if self.material != self._empty_material: search_criteria["name"] = self.material.name else: preferred_material = definition.getMetaDataEntry("preferred_material") @@ -503,7 +514,7 @@ class CuraContainerStack(ContainerStack): else: search_criteria["definition"] = "fdmprinter" - if self.quality != self._empty_instance_container: + if self.quality != self._empty_quality: search_criteria["name"] = self.quality.name else: preferred_quality = definition.getMetaDataEntry("preferred_quality") diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py index fe7068b7ea..42a2733879 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -92,8 +92,8 @@ class ExtruderStack(CuraContainerStack): return self.getNextStack()._getMachineDefinition() @override(CuraContainerStack) - def deserialize(self, contents: str) -> None: - super().deserialize(contents) + def deserialize(self, contents: str, file_name: Optional[str] = None) -> None: + super().deserialize(contents, file_name) stacks = ContainerRegistry.getInstance().findContainerStacks(id=self.getMetaDataEntry("machine", "")) if stacks: self.setNextStack(stacks[0]) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 002c84fb67..ca929b46fc 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -621,9 +621,16 @@ class MachineManager(QObject): def activeQualityId(self) -> str: if self._active_container_stack: quality = self._active_container_stack.quality + if isinstance(quality, type(self._empty_quality_container)): + return "" quality_changes = self._active_container_stack.qualityChanges - if quality and quality_changes and isinstance(quality_changes, type(self._empty_quality_changes_container)) and not isinstance(quality, type(self._empty_quality_container)): - return quality.getId() + if quality and quality_changes: + if isinstance(quality_changes, type(self._empty_quality_changes_container)): + # It's a built-in profile + return quality.getId() + else: + # Custom profile + return quality_changes.getId() return "" @pyqtProperty(str, notify=activeQualityChanged) diff --git a/cura/Settings/MaterialsModel.py b/cura/Settings/MaterialsModel.py index bee9307b53..bab8929765 100644 --- a/cura/Settings/MaterialsModel.py +++ b/cura/Settings/MaterialsModel.py @@ -18,4 +18,8 @@ class MaterialsModel(InstanceContainersModel): # \param container The container whose metadata was changed. def _onContainerMetaDataChanged(self, container): if container.getMetaDataEntry("type") == "material": #Only need to update if a material was changed. - self._update() \ No newline at end of file + self._update() + + def _onContainerChanged(self, container): + if container.getMetaDataEntry("type", "") == "material": + super()._onContainerChanged(container) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index a237460bab..dfb16b91e1 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -152,7 +152,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if not definitions: definition_container = DefinitionContainer(container_id) - definition_container.deserialize(archive.open(each_definition_container_file).read().decode("utf-8")) + definition_container.deserialize(archive.open(each_definition_container_file).read().decode("utf-8"), + file_name = each_definition_container_file) else: definition_container = definitions[0] @@ -208,7 +209,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): instance_container = InstanceContainer(container_id) # Deserialize InstanceContainer by converting read data from bytes to string - instance_container.deserialize(archive.open(each_instance_container_file).read().decode("utf-8")) + instance_container.deserialize(archive.open(each_instance_container_file).read().decode("utf-8"), + file_name = each_instance_container_file) instance_container_list.append(instance_container) container_type = instance_container.getMetaDataEntry("type") @@ -378,7 +380,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return WorkspaceReader.PreReadResult.accepted ## Overrides an ExtruderStack in the given GlobalStack and returns the new ExtruderStack. - def _overrideExtruderStack(self, global_stack, extruder_file_content): + def _overrideExtruderStack(self, global_stack, extruder_file_content, extruder_stack_file): # Get extruder position first extruder_config = configparser.ConfigParser() extruder_config.read_string(extruder_file_content) @@ -394,7 +396,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return None # Override the given extruder stack - extruder_stack.deserialize(extruder_file_content) + extruder_stack.deserialize(extruder_file_content, file_name = extruder_stack_file) # return the new ExtruderStack return extruder_stack @@ -484,7 +486,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): definitions = self._container_registry.findDefinitionContainers(id = container_id) if not definitions: definition_container = DefinitionContainer(container_id) - definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8")) + definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"), + file_name = definition_container_file) self._container_registry.addContainer(definition_container) Job.yieldThread() @@ -502,18 +505,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if not materials: material_container = xml_material_profile(container_id) - material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) + material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"), + file_name = material_container_file) containers_to_add.append(material_container) else: material_container = materials[0] if not material_container.isReadOnly(): # Only create new materials if they are not read only. if self._resolve_strategies["material"] == "override": - material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) + material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"), + file_name = material_container_file) elif self._resolve_strategies["material"] == "new": # Note that we *must* deserialize it with a new ID, as multiple containers will be # auto created & added. material_container = xml_material_profile(self.getNewId(container_id)) - material_container.deserialize(archive.open(material_container_file).read().decode("utf-8")) + material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"), + file_name = material_container_file) containers_to_add.append(material_container) material_containers.append(material_container) @@ -540,7 +546,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): instance_container = InstanceContainer(container_id) # Deserialize InstanceContainer by converting read data from bytes to string - instance_container.deserialize(serialized) + instance_container.deserialize(serialized, file_name = instance_container_file) container_type = instance_container.getMetaDataEntry("type") Job.yieldThread() @@ -562,7 +568,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: if self._resolve_strategies["machine"] == "override" or self._resolve_strategies["machine"] is None: instance_container = user_containers[0] - instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) + instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"), + file_name = instance_container_file) instance_container.setDirty(True) elif self._resolve_strategies["machine"] == "new": # The machine is going to get a spiffy new name, so ensure that the id's of user settings match. @@ -595,7 +602,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # selected strategy. if self._resolve_strategies[container_type] == "override": instance_container = changes_containers[0] - instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8")) + instance_container.deserialize(archive.open(instance_container_file).read().decode("utf-8"), + file_name = instance_container_file) instance_container.setDirty(True) elif self._resolve_strategies[container_type] == "new": @@ -656,7 +664,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # There is a machine, check if it has authentication data. If so, keep that data. network_authentication_id = container_stacks[0].getMetaDataEntry("network_authentication_id") network_authentication_key = container_stacks[0].getMetaDataEntry("network_authentication_key") - container_stacks[0].deserialize(archive.open(global_stack_file).read().decode("utf-8")) + container_stacks[0].deserialize(archive.open(global_stack_file).read().decode("utf-8"), + file_name = global_stack_file) if network_authentication_id: container_stacks[0].addMetaDataEntry("network_authentication_id", network_authentication_id) if network_authentication_key: @@ -666,7 +675,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # create a new global stack stack = GlobalStack(global_stack_id_new) # Deserialize stack by converting read data from bytes to string - stack.deserialize(archive.open(global_stack_file).read().decode("utf-8")) + stack.deserialize(archive.open(global_stack_file).read().decode("utf-8"), + file_name = global_stack_file) # Ensure a unique ID and name stack._id = global_stack_id_new @@ -706,7 +716,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if self._resolve_strategies["machine"] == "override": if global_stack.getProperty("machine_extruder_count", "value") > 1: # deserialize new extruder stack over the current ones (if any) - stack = self._overrideExtruderStack(global_stack, extruder_file_content) + stack = self._overrideExtruderStack(global_stack, extruder_file_content, extruder_stack_file) if stack is None: continue @@ -726,7 +736,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): extruder_config.write(tmp_string_io) extruder_file_content = tmp_string_io.getvalue() - stack.deserialize(extruder_file_content) + stack.deserialize(extruder_file_content, file_name = extruder_stack_file) # Ensure a unique ID and name stack._id = new_id @@ -741,12 +751,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if stack.definitionChanges == self._container_registry.getEmptyInstanceContainer(): stack.setDefinitionChanges(CuraStackBuilder.createDefinitionChangesContainer(stack, stack.getId() + "_settings")) - extruder_stacks.append(stack) + if stack.getMetaDataEntry("type") == "extruder_train": + extruder_stacks.append(stack) # 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: - self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder") + stack = self._container_registry.addExtruderStackForSingleExtrusionMachine(global_stack, "fdmextruder") + if stack: + extruder_stacks.append(stack) except: Logger.logException("w", "We failed to serialize the stack. Trying to clean up.") @@ -780,6 +793,46 @@ class ThreeMFWorkspaceReader(WorkspaceReader): for stack in [global_stack] + extruder_stacks: stack.replaceContainer(_ContainerIndexes.Quality, empty_quality_container) + # Fix quality: + # The quality specified in an old project file can be wrong, for example, for UM2, it should be "um2_normal" + # but instead it was "normal". This should be fixed by setting it to the correct quality. + # Note that this only seems to happen on single-extrusion machines on the global stack, so we only apply the + # fix for that + quality = global_stack.quality + if quality.getId() not in ("empty", "empty_quality"): + quality_type = quality.getMetaDataEntry("quality_type") + quality_containers = self._container_registry.findInstanceContainers(definition = global_stack.definition.getId(), + type = "quality", + quality_type = quality_type) + quality_containers = [q for q in quality_containers if q.getMetaDataEntry("material", "") == ""] + if quality_containers: + global_stack.quality = quality_containers[0] + else: + # look for "fdmprinter" qualities if the machine-specific qualities cannot be found + quality_containers = self._container_registry.findInstanceContainers(definition = "fdmprinter", + type = "quality", + quality_type = quality_type) + quality_containers = [q for q in quality_containers if q.getMetaDataEntry("material", "") == ""] + if quality_containers: + global_stack.quality = quality_containers[0] + else: + # the quality_type of the quality profile cannot be found. + # this can happen if a quality_type has been removed in a newer version, for example: + # "extra_coarse" is removed from 2.7 to 3.0 + # in this case, the quality will be reset to "normal" + quality_containers = self._container_registry.findInstanceContainers( + definition = global_stack.definition.getId(), + type = "quality", + quality_type = "normal") + quality_containers = [q for q in quality_containers if q.getMetaDataEntry("material", "") == ""] + if quality_containers: + global_stack.quality = quality_containers[0] + else: + # This should not happen! + Logger.log("e", "Cannot find quality normal for global stack [%s] [%s]", + global_stack.getId(), global_stack.definition.getId()) + global_stack.quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0] + # Replacing the old containers if resolve is "new". # When resolve is "new", some containers will get renamed, so all the other containers that reference to those # MUST get updated too. diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 51fcf3ec59..37ab451d16 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -95,22 +95,22 @@ class ProcessSlicedLayersJob(Job): # Find the minimum layer number # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we - # instead simply offset all other layers so the lowest layer is always 0. + # instead simply offset all other layers so the lowest layer is always 0. It could happens that + # the first raft layer has value -8 but there are just 4 raft (negative) layers. min_layer_number = 0 + negative_layers = 0 for layer in self._layers: if layer.id < min_layer_number: min_layer_number = layer.id + if layer.id < 0: + negative_layers += 1 current_layer = 0 for layer in self._layers: - abs_layer_number = layer.id + abs(min_layer_number) - - # Workaround when the last layer doesn't have paths, this layer is skipped because this was generating - # some glitches when rendering. - if layer.id == len(self._layers)-1 and layer.repeatedMessageCount("path_segment") == 0: - Logger.log("i", "No sliced data in the layer", layer.id) - continue + # Negative layers are offset by the minimum layer number, but the positive layers are just + # offset by the number of negative layers so there is no layer gap between raft and model + abs_layer_number = layer.id + abs(min_layer_number) if layer.id < 0 else layer.id + negative_layers layer_data.addLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number) diff --git a/plugins/GCodeReader/GCodeReader.py b/plugins/GCodeReader/GCodeReader.py index 2a7e29e370..d37769ec3a 100755 --- a/plugins/GCodeReader/GCodeReader.py +++ b/plugins/GCodeReader/GCodeReader.py @@ -143,6 +143,11 @@ class GCodeReader(MeshReader): this_layer.polygons.append(this_poly) return True + def _createEmptyLayer(self, layer_number): + self._layer_data_builder.addLayer(layer_number) + self._layer_data_builder.setLayerHeight(layer_number, 0) + self._layer_data_builder.setLayerThickness(layer_number, 0) + def _calculateLineWidth(self, current_point, previous_point, current_extrusion, previous_extrusion, layer_thickness): # Area of the filament Af = (self._filament_diameter / 2) ** 2 * numpy.pi @@ -322,6 +327,9 @@ class GCodeReader(MeshReader): current_position = self._position(0, 0, 0, 0, [0]) current_path = [] + min_layer_number = 0 + negative_layers = 0 + previous_layer = 0 for line in file: if self._cancelled: @@ -359,7 +367,23 @@ class GCodeReader(MeshReader): layer_number = int(line[len(self._layer_keyword):]) self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0])) current_path.clear() + + # When using a raft, the raft layers are stored as layers < 0, it mimics the same behavior + # as in ProcessSlicedLayersJob + if layer_number < min_layer_number: + min_layer_number = layer_number + if layer_number < 0: + layer_number += abs(min_layer_number) + negative_layers += 1 + else: + layer_number += negative_layers + + # In case there is a gap in the layer count, empty layers are created + for empty_layer in range(previous_layer + 1, layer_number): + self._createEmptyLayer(empty_layer) + self._layer_number = layer_number + previous_layer = layer_number except: pass diff --git a/plugins/SimulationView/SimulationPass.py b/plugins/SimulationView/SimulationPass.py index 2b9063e27a..46fa7f1240 100644 --- a/plugins/SimulationView/SimulationPass.py +++ b/plugins/SimulationView/SimulationPass.py @@ -34,6 +34,7 @@ class SimulationPass(RenderPass): self._nozzle_shader = None self._old_current_layer = 0 self._old_current_path = 0 + self._switching_layers = True # It tracks when the user is moving the layers' slider self._gl = OpenGL.getInstance().getBindingsObject() self._scene = Application.getInstance().getController().getScene() self._extruder_manager = ExtruderManager.getInstance() @@ -91,7 +92,7 @@ class SimulationPass(RenderPass): self.bind() - tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Solid) + tool_handle_batch = RenderBatch(self._tool_handle_shader, type = RenderBatch.RenderType.Overlay) head_position = None # Indicates the current position of the print head nozzle_node = None @@ -143,8 +144,10 @@ class SimulationPass(RenderPass): # All the layers but the current selected layer are rendered first if self._old_current_path != self._layer_view._current_path_num: self._current_shader = self._layer_shadow_shader + self._switching_layers = False if not self._layer_view.isSimulationRunning() and self._old_current_layer != self._layer_view._current_layer_num: self._current_shader = self._layer_shader + self._switching_layers = True layers_batch = RenderBatch(self._current_shader, type = RenderBatch.RenderType.Solid, mode = RenderBatch.RenderMode.Lines, range = (start, end)) layers_batch.addItem(node.getWorldTransformation(), layer_data) @@ -170,8 +173,9 @@ class SimulationPass(RenderPass): if len(batch.items) > 0: batch.render(self._scene.getActiveCamera()) - # The nozzle is drawn once we know the correct position - if not self._compatibility_mode and self._layer_view.getActivity() and nozzle_node is not None: + # The nozzle is drawn when once we know the correct position of the head, + # but the user is not using the layer slider, and the compatibility mode is not enabled + if not self._switching_layers and not self._compatibility_mode and self._layer_view.getActivity() and nozzle_node is not None: if head_position is not None: nozzle_node.setVisible(True) nozzle_node.setPosition(head_position) diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 2751ea4f60..b7bff91f9b 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -376,7 +376,7 @@ class SimulationView(View): if layer is None: return new_max_paths = layer.lineMeshElementCount() - if new_max_paths > 0 and new_max_paths != self._max_paths: + if new_max_paths >= 0 and new_max_paths != self._max_paths: self._max_paths = new_max_paths self.maxPathsChanged.emit() diff --git a/plugins/SimulationView/SimulationView.qml b/plugins/SimulationView/SimulationView.qml index 70c9a0849b..bc224f19e2 100644 --- a/plugins/SimulationView/SimulationView.qml +++ b/plugins/SimulationView/SimulationView.qml @@ -138,10 +138,11 @@ Item text: catalog.i18nc("@label:listbox", "Feedrate"), type_id: 2 }) - layerViewTypes.append({ - text: catalog.i18nc("@label:listbox", "Layer thickness"), - type_id: 3 // these ids match the switching in the shader - }) + // TODO DON'T DELETE!!!! This part must be enabled when adaptive layer height feature is available +// layerViewTypes.append({ +// text: catalog.i18nc("@label:listbox", "Layer thickness"), +// type_id: 3 // these ids match the switching in the shader +// }) } ComboBox @@ -619,7 +620,7 @@ Item Timer { id: simulationTimer - interval: 250 + interval: 100 running: false repeat: true onTriggered: { diff --git a/plugins/SimulationView/__init__.py b/plugins/SimulationView/__init__.py index f7ccf41acc..15e113bd8e 100644 --- a/plugins/SimulationView/__init__.py +++ b/plugins/SimulationView/__init__.py @@ -11,7 +11,7 @@ catalog = i18nCatalog("cura") def getMetaData(): return { "view": { - "name": catalog.i18nc("@item:inlistbox", "Simulation view"), + "name": catalog.i18nc("@item:inlistbox", "Layer view"), "view_panel": "SimulationView.qml", "weight": 2 } diff --git a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py index 7b788f96ba..c496a66b29 100644 --- a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py +++ b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py @@ -116,7 +116,10 @@ class VersionUpgrade30to31(VersionUpgrade): 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. - if len(all_quality_changes) == 1 and not parser.has_option("metadata", "extruder"): + # 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) # Update version numbers @@ -199,7 +202,7 @@ class VersionUpgrade30to31(VersionUpgrade): def _createExtruderQualityChangesForSingleExtrusionMachine(self, filename, global_quality_changes): suffix = "_" + quote_plus(global_quality_changes["general"]["name"].lower()) - machine_name = filename.strip("." + os.sep).replace(suffix, "") + machine_name = os.path.os.path.basename(filename).replace(".inst.cfg", "").replace(suffix, "") new_filename = machine_name + "_" + "fdmextruder" + suffix extruder_quality_changes_parser = configparser.ConfigParser() diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index fa40819eeb..7ab4520aea 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -422,11 +422,11 @@ class XmlMaterialProfile(InstanceContainer): return version * 1000000 + setting_version ## Overridden from InstanceContainer - def deserialize(self, serialized): + def deserialize(self, serialized, file_name = None): containers_to_add = [] # update the serialized data first from UM.Settings.Interfaces import ContainerInterface - serialized = ContainerInterface.deserialize(self, serialized) + serialized = ContainerInterface.deserialize(self, serialized, file_name) try: data = ET.fromstring(serialized) diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index ad6c2ce050..dc5853ebb2 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -159,7 +159,7 @@ UM.PreferencesPage append({ text: "Nederlands", code: "nl_NL" }) append({ text: "Polski", code: "pl_PL" }) append({ text: "Português do Brasil", code: "pt_BR" }) - append({ text: "Русский", code: "ru_RU" }) + //Russian is disabled for being incomplete: append({ text: "Русский", code: "ru_RU" }) append({ text: "Türkçe", code: "tr_TR" }) append({ text: "简体中文", code: "zh_CN" }) diff --git a/resources/qml/Preferences/MaterialsPage.qml b/resources/qml/Preferences/MaterialsPage.qml index 5e014faf24..cd04b79b20 100644 --- a/resources/qml/Preferences/MaterialsPage.qml +++ b/resources/qml/Preferences/MaterialsPage.qml @@ -322,7 +322,6 @@ UM.ManagementPage { messageDialog.icon = StandardIcon.Information messageDialog.text = catalog.i18nc("@info:status Don't translate the XML tag !", "Successfully imported material %1").arg(fileUrl) - currentItem = base.model.getItem(base.objectList.currentIndex) } else if(result.status == "duplicate") { diff --git a/resources/qml/Topbar.qml b/resources/qml/Topbar.qml index c69c786d5a..6085c6fe7e 100644 --- a/resources/qml/Topbar.qml +++ b/resources/qml/Topbar.qml @@ -220,6 +220,83 @@ Rectangle menu: PrinterMenu { } } + //View orientation Item + Row + { + id: viewOrientationControl + height: 30 + + spacing: 2 + + visible: !base.monitoringPrint + + anchors { + verticalCenter: base.verticalCenter + right: viewModeButton.right + rightMargin: UM.Theme.getSize("default_margin").width + viewModeButton.width + } + + // #1 3d view + Button + { + iconSource: UM.Theme.getIcon("view_3d") + style: UM.Theme.styles.orientation_button + anchors.verticalCenter: viewOrientationControl.verticalCenter + onClicked:{ + UM.Controller.rotateView("3d", 0); + } + visible: base.width > 1100 + } + + // #2 Front view + Button + { + iconSource: UM.Theme.getIcon("view_front") + style: UM.Theme.styles.orientation_button + anchors.verticalCenter: viewOrientationControl.verticalCenter + onClicked:{ + UM.Controller.rotateView("home", 0); + } + visible: base.width > 1130 + } + + // #3 Top view + Button + { + iconSource: UM.Theme.getIcon("view_top") + style: UM.Theme.styles.orientation_button + anchors.verticalCenter: viewOrientationControl.verticalCenter + onClicked:{ + UM.Controller.rotateView("y", 90); + } + visible: base.width > 1160 + } + + // #4 Left view + Button + { + iconSource: UM.Theme.getIcon("view_left") + style: UM.Theme.styles.orientation_button + anchors.verticalCenter: viewOrientationControl.verticalCenter + onClicked:{ + UM.Controller.rotateView("x", 90); + } + visible: base.width > 1190 + } + + // #5 Left view + Button + { + iconSource: UM.Theme.getIcon("view_right") + style: UM.Theme.styles.orientation_button + anchors.verticalCenter: viewOrientationControl.verticalCenter + onClicked:{ + UM.Controller.rotateView("x", -90); + } + visible: base.width > 1210 + } + } + ComboBox { id: viewModeButton diff --git a/resources/themes/cura-light/icons/view_3d.svg b/resources/themes/cura-light/icons/view_3d.svg new file mode 100644 index 0000000000..cfe394e65d --- /dev/null +++ b/resources/themes/cura-light/icons/view_3d.svg @@ -0,0 +1,14 @@ + + + + icn_perspectives_white + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/icons/view_front.svg b/resources/themes/cura-light/icons/view_front.svg new file mode 100644 index 0000000000..7de9abe0af --- /dev/null +++ b/resources/themes/cura-light/icons/view_front.svg @@ -0,0 +1,19 @@ + + + + icn_front_white + Created with Sketch. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/icons/view_left.svg b/resources/themes/cura-light/icons/view_left.svg new file mode 100644 index 0000000000..1770da4c81 --- /dev/null +++ b/resources/themes/cura-light/icons/view_left.svg @@ -0,0 +1,21 @@ + + + + icn_left_white + Created with Sketch. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/icons/view_right.svg b/resources/themes/cura-light/icons/view_right.svg new file mode 100644 index 0000000000..5e0628e60e --- /dev/null +++ b/resources/themes/cura-light/icons/view_right.svg @@ -0,0 +1,21 @@ + + + + icn_right_white + Created with Sketch. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/icons/view_top.svg b/resources/themes/cura-light/icons/view_top.svg new file mode 100644 index 0000000000..3eb32e9878 --- /dev/null +++ b/resources/themes/cura-light/icons/view_top.svg @@ -0,0 +1,21 @@ + + + + icn_top_white + Created with Sketch. + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/styles.qml b/resources/themes/cura-light/styles.qml index ea9d184926..0f3c910270 100755 --- a/resources/themes/cura-light/styles.qml +++ b/resources/themes/cura-light/styles.qml @@ -381,6 +381,111 @@ QtObject { } } + property Component orientation_button: Component { + ButtonStyle { + background: Item { + implicitWidth: 25; + implicitHeight: 25; + + Rectangle { + id: buttonFace2; + + anchors.fill: parent; + property bool down: control.pressed || (control.checkable && control.checked); + + color: { + if(control.customColor !== undefined && control.customColor !== null) { + return control.customColor + } else if(control.checkable && control.checked && control.hovered) { + return Theme.getColor("button_active_hover"); + } else if(control.pressed || (control.checkable && control.checked)) { + return Theme.getColor("button_active"); + } else if(control.hovered) { + return Theme.getColor("button_hover"); + } else { + //return Theme.getColor("button"); + return "transparent" + } + } + Behavior on color { ColorAnimation { duration: 50; } } + + border.width: (control.hasOwnProperty("needBorder") && control.needBorder) ? 2 * screenScaleFactor : 0 + border.color: Theme.getColor("tool_button_border") + + UM.RecolorImage { + id: tool_button_arrow2 + //anchors.right: parent.right; + //anchors.rightMargin: (Theme.getSize("button").width - Theme.getSize("button_icon").width) / 4 + //anchors.bottom: parent.bottom; + //anchors.bottomMargin: (Theme.getSize("button").height - Theme.getSize("button_icon").height) / 4 + //width: Theme.getSize("standard_arrow").width + //height: Theme.getSize("standard_arrow").height + + width: 5 + height: 5 + + sourceSize.width: 5 + sourceSize.height: 5 + visible: control.menu != null; + color: + { + if(control.checkable && control.checked && control.hovered) + { + return Theme.getColor("button_text_active_hover"); + } + else if(control.pressed || (control.checkable && control.checked)) + { + return Theme.getColor("button_text_active"); + } + else if(control.hovered) + { + return Theme.getColor("button_text_hover"); + } + else + { + return Theme.getColor("button_text"); + } + } + source: Theme.getIcon("arrow_bottom") + } + } + } + + label: Item { + UM.RecolorImage { + anchors.centerIn: parent; + opacity: !control.enabled ? 0.2 : 1.0 + source: control.iconSource; + width: 20; + height: 20; + color: + { + if(control.checkable && control.checked && control.hovered) + { + return Theme.getColor("button_text_active_hover"); + } + else if(control.pressed || (control.checkable && control.checked)) + { + return Theme.getColor("button_text_active"); + } + else if(control.hovered) + { + //return Theme.getColor("button_text_hover"); + return "white" + } + else + { + //return Theme.getColor("button_text"); + return "black" + } + } + + sourceSize: Theme.getSize("button_icon") + } + } + } + } + property Component progressbar: Component{ ProgressBarStyle { background: Rectangle { @@ -753,6 +858,49 @@ QtObject { } } + property Component partially_checkbox: Component { + CheckBoxStyle { + background: Item { } + indicator: Rectangle { + implicitWidth: Theme.getSize("checkbox").width; + implicitHeight: Theme.getSize("checkbox").height; + + color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_hover") : Theme.getColor("checkbox"); + Behavior on color { ColorAnimation { duration: 50; } } + + radius: control.exclusiveGroup ? Theme.getSize("checkbox").width / 2 : 0 + + border.width: Theme.getSize("default_lining").width; + border.color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_border_hover") : Theme.getColor("checkbox_border"); + + UM.RecolorImage { + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width / 2.5 + height: parent.height / 2.5 + sourceSize.width: width + sourceSize.height: width + color: Theme.getColor("checkbox_mark") + source: { + if (control.checkbox_state == 2){ + return Theme.getIcon("solid") + } + else{ + return control.exclusiveGroup ? Theme.getIcon("dot") : Theme.getIcon("check") + } + } + opacity: control.checked + Behavior on opacity { NumberAnimation { duration: 100; } } + } + } + label: Label { + text: control.text; + color: Theme.getColor("checkbox_text"); + font: Theme.getFont("default"); + } + } + } + property Component slider: Component { SliderStyle { groove: Rectangle {