Merge branch 'master' into CURA-5204_send_anonymous_data

This commit is contained in:
Diego Prado Gesto 2018-04-20 10:35:52 +02:00
commit 4e53f219f0
100 changed files with 4143 additions and 4744 deletions

View file

@ -7,6 +7,7 @@ import os
import threading
from typing import List, Tuple
import xml.etree.ElementTree as ET
from UM.Workspace.WorkspaceReader import WorkspaceReader
@ -15,6 +16,7 @@ from UM.Application import Application
from UM.Logger import Logger
from UM.i18n import i18nCatalog
from UM.Signal import postponeSignals, CompressTechnique
from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer
@ -28,43 +30,13 @@ from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.CuraContainerStack import _ContainerIndexes
from cura.CuraApplication import CuraApplication
from cura.Utils.Threading import call_on_qt_thread
from .WorkspaceDialog import WorkspaceDialog
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
class ContainerInfo:
def __init__(self, file_name: str, serialized: str, parser: ConfigParser):
self.file_name = file_name
@ -332,7 +304,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
containers_found_dict["quality_changes"] = True
# Check if there really is a conflict by comparing the values
instance_container = InstanceContainer(container_id)
instance_container.deserialize(serialized, file_name = instance_container_file_name)
try:
instance_container.deserialize(serialized, file_name = instance_container_file_name)
except ContainerFormatError:
Logger.logException("e", "Failed to deserialize InstanceContainer %s from project file %s",
instance_container_file_name, file_name)
return ThreeMFWorkspaceReader.PreReadResult.failed
if quality_changes[0] != instance_container:
quality_changes_conflict = True
elif container_type == "quality":
@ -639,8 +616,15 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id)
if not definitions:
definition_container = DefinitionContainer(container_id)
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
file_name = definition_container_file)
try:
definition_container.deserialize(archive.open(definition_container_file).read().decode("utf-8"),
file_name = definition_container_file)
except ContainerFormatError:
# We cannot just skip the definition file because everything else later will just break if the
# machine definition cannot be found.
Logger.logException("e", "Failed to deserialize definition file %s in project file %s",
definition_container_file, file_name)
definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
self._container_registry.addContainer(definition_container)
Job.yieldThread()
@ -679,8 +663,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if to_deserialize_material:
material_container = xml_material_profile(container_id)
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
file_name = container_id + "." + self._material_container_suffix)
try:
material_container.deserialize(archive.open(material_container_file).read().decode("utf-8"),
file_name = container_id + "." + self._material_container_suffix)
except ContainerFormatError:
Logger.logException("e", "Failed to deserialize material file %s in project file %s",
material_container_file, file_name)
continue
if need_new_name:
new_name = ContainerRegistry.getInstance().uniqueName(material_container.getName())
material_container.setName(new_name)
@ -704,7 +693,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# To solve this, we schedule _updateActiveMachine() for later so it will have the latest data.
self._updateActiveMachine(global_stack)
# Load all the nodes / meshdata of the workspace
# Load all the nodes / mesh data of the workspace
nodes = self._3mf_mesh_reader.read(file_name)
if nodes is None:
nodes = []

View file

@ -6,16 +6,18 @@ 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 UM.Workspace.WorkspaceWriter import WorkspaceWriter
from cura.Utils.Threading import call_on_qt_thread
class ThreeMFWorkspaceWriter(WorkspaceWriter):
def __init__(self):
super().__init__()
@call_on_qt_thread
def write(self, stream, nodes, mode=WorkspaceWriter.OutputMode.BinaryMode):
application = Application.getInstance()
machine_manager = application.getMachineManager()

View file

@ -23,9 +23,9 @@ class ChangeLog(Extension, QObject,):
self._changelog_context = None
version_string = Application.getInstance().getVersion()
if version_string is not "master":
self._version = Version(version_string)
self._current_app_version = Version(version_string)
else:
self._version = None
self._current_app_version = None
self._change_logs = None
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
@ -76,7 +76,7 @@ class ChangeLog(Extension, QObject,):
self._change_logs[open_version][open_header].append(line)
def _onEngineCreated(self):
if not self._version:
if not self._current_app_version:
return #We're on dev branch.
if Preferences.getInstance().getValue("general/latest_version_changelog_shown") == "master":
@ -91,7 +91,7 @@ class ChangeLog(Extension, QObject,):
if not Application.getInstance().getGlobalContainerStack():
return
if self._version > latest_version_shown:
if self._current_app_version > latest_version_shown:
self.showChangelog()
def showChangelog(self):

View file

@ -121,7 +121,7 @@ class CuraEngineBackend(QObject, Backend):
self._slice_start_time = None
self._is_disabled = False
Preferences.getInstance().addPreference("general/auto_slice", True)
Preferences.getInstance().addPreference("general/auto_slice", False)
self._use_timer = False
# When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired.

View file

@ -1,9 +1,10 @@
# Copyright (c) 2016 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser
from UM.PluginRegistry import PluginRegistry
from UM.Logger import Logger
from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
from cura.ProfileReader import ProfileReader
@ -77,7 +78,10 @@ class CuraProfileReader(ProfileReader):
profile.addMetaDataEntry("type", "quality_changes")
try:
profile.deserialize(serialized)
except Exception as e: # Parsing error. This is not a (valid) Cura profile then.
except ContainerFormatError as e:
Logger.log("e", "Error in the format of a container: %s", str(e))
return None
except Exception as e:
Logger.log("e", "Error while trying to parse profile: %s", str(e))
return None
return profile

View file

@ -1,9 +1,10 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import re #Regular expressions for parsing escape characters in the settings.
import json
from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Logger import Logger
from UM.i18n import i18nCatalog
@ -113,6 +114,9 @@ def readQualityProfileFromString(profile_string):
profile = InstanceContainer("")
try:
profile.deserialize(profile_string)
except ContainerFormatError as e:
Logger.log("e", "Corrupt profile in this g-code file: %s", str(e))
return None
except Exception as e: # Not a valid g-code file.
Logger.log("e", "Unable to serialise the profile: %s", str(e))
return None

View file

@ -41,11 +41,11 @@ class FlavorParser:
self._is_layers_in_file = False # Does the Gcode have the layers comment?
self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._current_layer_thickness = 0.2 # default
self._filament_diameter = 2.85 # default
Preferences.getInstance().addPreference("gcodereader/show_caution", True)
def _clearValues(self):
self._filament_diameter = 2.85
self._extruder_number = 0
self._extrusion_length_offset = [0]
self._layer_type = LayerPolygon.Inset0Type
@ -289,8 +289,9 @@ class FlavorParser:
def processGCodeStream(self, stream):
Logger.log("d", "Preparing to load GCode")
self._cancelled = False
# We obtain the filament diameter from the selected printer to calculate line widths
self._filament_diameter = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value")
# We obtain the filament diameter from the selected extruder to calculate line widths
global_stack = Application.getInstance().getGlobalContainerStack()
self._filament_diameter = global_stack.extruders[str(self._extruder_number)].getProperty("material_diameter", "value")
scene_node = CuraSceneNode()
# Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no
@ -309,10 +310,9 @@ class FlavorParser:
current_line = 0
for line in stream.split("\n"):
file_lines += 1
gcode_list.append(line)
gcode_list.append(line + "\n")
if not self._is_layers_in_file and line[:len(self._layer_keyword)] == self._layer_keyword:
self._is_layers_in_file = True
# stream.seek(0)
file_step = max(math.floor(file_lines / 100), 1)

View file

@ -18,10 +18,10 @@ class RepRapFlavorParser(FlavorParser.FlavorParser):
self._is_absolute_extrusion = False
## Set the absolute positioning
# RepRapFlavor code G90 sets position of X, Y, Z, and E to absolute
# RepRapFlavor code G90 sets position of X, Y, Z to absolute
# For absolute E, M82 is used
def _gCode90(self, position, params, path):
self._is_absolute_positioning = True
self._is_absolute_extrusion = True
return position
## Set the relative positioning

View file

@ -42,6 +42,8 @@ class GCodeWriter(MeshWriter):
re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors.
}
_setting_keyword = ";SETTING_"
def __init__(self):
super().__init__()
@ -69,11 +71,15 @@ class GCodeWriter(MeshWriter):
return False
gcode_list = gcode_dict.get(active_build_plate, None)
if gcode_list is not None:
has_settings = False
for gcode in gcode_list:
if gcode[:len(self._setting_keyword)] == self._setting_keyword:
has_settings = True
stream.write(gcode)
# Serialise the current container stack and put it at the end of the file.
settings = self._serialiseSettings(Application.getInstance().getGlobalContainerStack())
stream.write(settings)
if not has_settings:
settings = self._serialiseSettings(Application.getInstance().getGlobalContainerStack())
stream.write(settings)
return True
return False
@ -108,7 +114,7 @@ class GCodeWriter(MeshWriter):
container_registry = self._application.getContainerRegistry()
quality_manager = self._application.getQualityManager()
prefix = ";SETTING_" + str(GCodeWriter.version) + " " # The prefix to put before each line.
prefix = self._setting_keyword + str(GCodeWriter.version) + " " # The prefix to put before each line.
prefix_length = len(prefix)
quality_type = stack.quality.getMetaDataEntry("quality_type")

View file

@ -49,6 +49,13 @@ class ModelChecker(QObject, Extension):
warning_size_xy = 150 #The horizontal size of a model that would be too large when dealing with shrinking materials.
warning_size_z = 100 #The vertical size of a model that would be too large when dealing with shrinking materials.
# This function can be triggered in the middle of a machine change, so do not proceed if the machine change
# has not done yet.
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack is None:
Application.getInstance().callLater(lambda: self.onChanged.emit())
return False
material_shrinkage = self._getMaterialShrinkage()
warning_nodes = []
@ -56,6 +63,13 @@ class ModelChecker(QObject, Extension):
# Check node material shrinkage and bounding box size
for node in self.sliceableNodes():
node_extruder_position = node.callDecoration("getActiveExtruderPosition")
# This function can be triggered in the middle of a machine change, so do not proceed if the machine change
# has not done yet.
if str(node_extruder_position) not in global_container_stack.extruders:
Application.getInstance().callLater(lambda: self.onChanged.emit())
return False
if material_shrinkage[node_extruder_position] > shrinkage_threshold:
bbox = node.getBoundingBox()
if bbox.width >= warning_size_xy or bbox.depth >= warning_size_xy or bbox.height >= warning_size_z:
@ -63,11 +77,11 @@ class ModelChecker(QObject, Extension):
self._caution_message.setText(catalog.i18nc(
"@info:status",
"Some models may not be printed optimally due to object size and chosen material for models: {model_names}.\n"
"Tips that may be useful to improve the print quality:\n"
"1) Use rounded corners.\n"
"2) Turn the fan off (only if there are no tiny details on the model).\n"
"3) Use a different material.").format(model_names = ", ".join([n.getName() for n in warning_nodes])))
"<p>One or more 3D models may not print optimally due to the model size and material configuration:</p>\n"
"<p>{model_names}</p>\n"
"<p>Find out how to ensure the best possible print quality and reliability.</p>\n"
"<p><a href=\"https://ultimaker.com/3D-model-assistant\">View print quality guide</a></p>"
).format(model_names = ", ".join([n.getName() for n in warning_nodes])))
return len(warning_nodes) > 0
@ -92,9 +106,8 @@ class ModelChecker(QObject, Extension):
Logger.log("d", "Model checker view created.")
@pyqtProperty(bool, notify = onChanged)
def runChecks(self):
def hasWarnings(self):
danger_shrinkage = self.checkObjectsForShrinkage()
return any((danger_shrinkage, )) #If any of the checks fail, show the warning button.
@pyqtSlot()

View file

@ -18,7 +18,7 @@ Button
UM.I18nCatalog{id: catalog; name:"cura"}
visible: manager.runChecks
visible: manager.hasWarnings
tooltip: catalog.i18nc("@info:tooltip", "Some things could be problematic in this print. Click to see tips for adjustment.")
onClicked: manager.showWarnings()

View file

@ -15,6 +15,7 @@ Window {
id: base
title: catalog.i18nc("@title:tab", "Plugins");
modality: Qt.ApplicationModal
width: 800 * screenScaleFactor
height: 640 * screenScaleFactor
minimumWidth: 350 * screenScaleFactor

View file

@ -1,12 +1,12 @@
# Copyright (c) 2015 Jaime van Kessel
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from UM.Logger import Logger
from UM.Signal import Signal, signalemitter
from UM.i18n import i18nCatalog
# Setting stuff import
from UM.Application import Application
from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.DefinitionContainer import DefinitionContainer
@ -39,8 +39,12 @@ class Script:
self._definition = definitions[0]
else:
self._definition = DefinitionContainer(setting_data["key"])
self._definition.deserialize(json.dumps(setting_data))
ContainerRegistry.getInstance().addContainer(self._definition)
try:
self._definition.deserialize(json.dumps(setting_data))
ContainerRegistry.getInstance().addContainer(self._definition)
except ContainerFormatError:
self._definition = None
return
self._stack.addContainer(self._definition)
self._instance = InstanceContainer(container_id="ScriptInstanceContainer")
self._instance.setDefinition(self._definition.getId())

View file

@ -84,7 +84,10 @@ class SolidView(View):
per_mesh_stack = node.callDecoration("getStack")
extruder_index = int(node.callDecoration("getActiveExtruderPosition"))
extruder_index = node.callDecoration("getActiveExtruderPosition")
if extruder_index is None:
extruder_index = "0"
extruder_index = int(extruder_index)
# Use the support extruder instead of the active extruder if this is a support_mesh
if per_mesh_stack:

View file

@ -19,8 +19,10 @@ from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.PickingPass import PickingPass
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from cura.Operations.SetParentOperation import SetParentOperation
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
@ -56,7 +58,7 @@ class SupportEraser(Tool):
modifiers = QApplication.keyboardModifiers()
ctrl_is_active = modifiers & Qt.ControlModifier
if event.type == Event.MousePressEvent and self._controller.getToolsEnabled():
if event.type == Event.MousePressEvent and MouseEvent.LeftButton in event.buttons and self._controller.getToolsEnabled():
if ctrl_is_active:
self._controller.setActiveTool("TranslateTool")
return
@ -117,7 +119,10 @@ class SupportEraser(Tool):
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
op = AddSceneNodeOperation(node, parent)
op = GroupedOperation()
# First add node to the scene at the correct position/scale, before parenting, so the eraser mesh does not get scaled with the parent
op.addOperation(AddSceneNodeOperation(node, self._controller.getScene().getRoot()))
op.addOperation(SetParentOperation(node, parent))
op.push()
node.setPosition(position, CuraSceneNode.TransformSpace.World)

View file

@ -104,6 +104,9 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if self._is_printing:
return # Aleady printing
# cancel any ongoing preheat timer before starting a print
self._printers[0].getController().stopPreheatTimers()
Application.getInstance().getController().setActiveStage("MonitorStage")
# find the G-code for the active build plate to print

View file

@ -4,6 +4,8 @@
import configparser #To parse preference files.
import io #To serialise the preference files afterwards.
import os
import urllib.parse
import re
from UM.VersionUpgrade import VersionUpgrade #We're inheriting from this.
@ -118,6 +120,12 @@ class VersionUpgrade27to30(VersionUpgrade):
if not parser.has_section("general"):
parser.add_section("general")
# Clean up the filename
file_base_name = os.path.basename(filename)
file_base_name = urllib.parse.unquote_plus(file_base_name)
um2_pattern = re.compile(r"^ultimaker[^a-zA-Z\\d\\s:]2_.*$")
# The ultimaker 2 family
ultimaker2_prefix_list = ["ultimaker2_extended_",
"ultimaker2_go_",
@ -127,9 +135,8 @@ class VersionUpgrade27to30(VersionUpgrade):
"ultimaker2_plus_"]
# set machine definition to "ultimaker2" for the custom quality profiles that can be for the ultimaker 2 family
file_base_name = os.path.basename(filename)
is_ultimaker2_family = False
if not any(file_base_name.startswith(ep) for ep in exclude_prefix_list):
is_ultimaker2_family = um2_pattern.match(file_base_name) is not None
if not is_ultimaker2_family and not any(file_base_name.startswith(ep) for ep in exclude_prefix_list):
is_ultimaker2_family = any(file_base_name.startswith(ep) for ep in ultimaker2_prefix_list)
# ultimaker2 family quality profiles used to set as "fdmprinter" profiles