Browse Source

Merge branch 'master' into python_type_hinting

Simon Edwards 8 years ago
parent
commit
eb43806d7a

+ 51 - 25
cura/BuildVolume.py

@@ -32,16 +32,18 @@ PRIME_CLEARANCE = 6.5
 
 ##  Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas.
 class BuildVolume(SceneNode):
-    VolumeOutlineColor = Color(12, 169, 227, 255)
-    XAxisColor = Color(255, 0, 0, 255)
-    YAxisColor = Color(0, 0, 255, 255)
-    ZAxisColor = Color(0, 255, 0, 255)
-
     raftThicknessChanged = Signal()
 
     def __init__(self, parent = None):
         super().__init__(parent)
 
+        self._volume_outline_color = None
+        self._x_axis_color = None
+        self._y_axis_color = None
+        self._z_axis_color = None
+        self._disallowed_area_color = None
+        self._error_area_color = None
+
         self._width = 0
         self._height = 0
         self._depth = 0
@@ -73,6 +75,9 @@ class BuildVolume(SceneNode):
         Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged)
         self._onStackChanged()
 
+        self._engine_ready = False
+        Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
+
         self._has_errors = False
         Application.getInstance().getController().getScene().sceneChanged.connect(self._onSceneChanged)
 
@@ -97,6 +102,7 @@ class BuildVolume(SceneNode):
         # but it does not update the disallowed areas after material change
         Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)
 
+
     def _onSceneChanged(self, source):
         if self._global_container_stack:
             self._change_timer.start()
@@ -156,6 +162,9 @@ class BuildVolume(SceneNode):
         if not self._shader:
             self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "default.shader"))
             self._grid_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "grid.shader"))
+            theme = Application.getInstance().getTheme()
+            self._grid_shader.setUniformValue("u_gridColor0", Color(*theme.getColor("buildplate").getRgb()))
+            self._grid_shader.setUniformValue("u_gridColor1", Color(*theme.getColor("buildplate_alt").getRgb()))
 
         renderer.queueNode(self, mode = RenderBatch.RenderMode.Lines)
         renderer.queueNode(self, mesh = self._origin_mesh)
@@ -174,6 +183,18 @@ class BuildVolume(SceneNode):
         if not self._width or not self._height or not self._depth:
             return
 
+        if not Application.getInstance()._engine:
+            return
+
+        if not self._volume_outline_color:
+            theme = Application.getInstance().getTheme()
+            self._volume_outline_color = Color(*theme.getColor("volume_outline").getRgb())
+            self._x_axis_color = Color(*theme.getColor("x_axis").getRgb())
+            self._y_axis_color = Color(*theme.getColor("y_axis").getRgb())
+            self._z_axis_color = Color(*theme.getColor("z_axis").getRgb())
+            self._disallowed_area_color = Color(*theme.getColor("disallowed_area").getRgb())
+            self._error_area_color = Color(*theme.getColor("error_area").getRgb())
+
         min_w = -self._width / 2
         max_w = self._width / 2
         min_h = 0.0
@@ -186,20 +207,20 @@ class BuildVolume(SceneNode):
         if self._shape != "elliptic":
             # Outline 'cube' of the build volume
             mb = MeshBuilder()
-            mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self.VolumeOutlineColor)
-            mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self.VolumeOutlineColor)
-            mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor)
-            mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self.VolumeOutlineColor)
+            mb.addLine(Vector(min_w, min_h, min_d), Vector(max_w, min_h, min_d), color = self._volume_outline_color)
+            mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, max_h, min_d), color = self._volume_outline_color)
+            mb.addLine(Vector(min_w, max_h, min_d), Vector(max_w, max_h, min_d), color = self._volume_outline_color)
+            mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, max_h, min_d), color = self._volume_outline_color)
 
-            mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor)
-            mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor)
-            mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
-            mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
+            mb.addLine(Vector(min_w, min_h, max_d), Vector(max_w, min_h, max_d), color = self._volume_outline_color)
+            mb.addLine(Vector(min_w, min_h, max_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color)
+            mb.addLine(Vector(min_w, max_h, max_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
+            mb.addLine(Vector(max_w, min_h, max_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
 
-            mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self.VolumeOutlineColor)
-            mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self.VolumeOutlineColor)
-            mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self.VolumeOutlineColor)
-            mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self.VolumeOutlineColor)
+            mb.addLine(Vector(min_w, min_h, min_d), Vector(min_w, min_h, max_d), color = self._volume_outline_color)
+            mb.addLine(Vector(max_w, min_h, min_d), Vector(max_w, min_h, max_d), color = self._volume_outline_color)
+            mb.addLine(Vector(min_w, max_h, min_d), Vector(min_w, max_h, max_d), color = self._volume_outline_color)
+            mb.addLine(Vector(max_w, max_h, min_d), Vector(max_w, max_h, max_d), color = self._volume_outline_color)
 
             self.setMeshData(mb.build())
 
@@ -226,8 +247,8 @@ class BuildVolume(SceneNode):
                 aspect = self._depth / self._width
                 scale_matrix.compose(scale = Vector(1, 1, aspect))
             mb = MeshBuilder()
-            mb.addArc(max_w, Vector.Unit_Y, center = (0, min_h - z_fight_distance, 0), color = self.VolumeOutlineColor)
-            mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0),  color = self.VolumeOutlineColor)
+            mb.addArc(max_w, Vector.Unit_Y, center = (0, min_h - z_fight_distance, 0), color = self._volume_outline_color)
+            mb.addArc(max_w, Vector.Unit_Y, center = (0, max_h, 0),  color = self._volume_outline_color)
             self.setMeshData(mb.build().getTransformed(scale_matrix))
 
             # Build plate grid mesh
@@ -258,21 +279,21 @@ class BuildVolume(SceneNode):
             height = self._origin_line_width,
             depth = self._origin_line_width,
             center = origin + Vector(self._origin_line_length / 2, 0, 0),
-            color = self.XAxisColor
+            color = self._x_axis_color
         )
         mb.addCube(
             width = self._origin_line_width,
             height = self._origin_line_length,
             depth = self._origin_line_width,
             center = origin + Vector(0, self._origin_line_length / 2, 0),
-            color = self.YAxisColor
+            color = self._y_axis_color
         )
         mb.addCube(
             width = self._origin_line_width,
             height = self._origin_line_width,
             depth = self._origin_line_length,
             center = origin - Vector(0, 0, self._origin_line_length / 2),
-            color = self.ZAxisColor
+            color = self._z_axis_color
         )
         self._origin_mesh = mb.build()
 
@@ -280,7 +301,7 @@ class BuildVolume(SceneNode):
         disallowed_area_size = 0
         if self._disallowed_areas:
             mb = MeshBuilder()
-            color = Color(0.0, 0.0, 0.0, 0.15)
+            color = self._disallowed_area_color
             for polygon in self._disallowed_areas:
                 points = polygon.getPoints()
                 if len(points) == 0:
@@ -309,7 +330,7 @@ class BuildVolume(SceneNode):
         if self._error_areas:
             mb = MeshBuilder()
             for error_area in self._error_areas:
-                color = Color(1.0, 0.0, 0.0, 0.5)
+                color = self._error_area_color
                 points = error_area.getPoints()
                 first = Vector(self._clamp(points[0][0], min_w, max_w), disallowed_area_height,
                                self._clamp(points[0][1], min_d, max_d))
@@ -396,7 +417,12 @@ class BuildVolume(SceneNode):
             self._updateDisallowedAreas()
             self._updateRaftThickness()
 
-            self.rebuild()
+            if self._engine_ready:
+                self.rebuild()
+
+    def _onEngineCreated(self):
+        self._engine_ready = True
+        self.rebuild()
 
     def _onSettingPropertyChanged(self, setting_key, property_name):
         if property_name != "value":

+ 3 - 2
cura/ConvexHullNode.py

@@ -1,6 +1,7 @@
 # Copyright (c) 2015 Ultimaker B.V.
 # Cura is released under the terms of the AGPLv3 or higher.
 
+from UM.Application import Application
 from UM.Scene.SceneNode import SceneNode
 from UM.Resources import Resources
 from UM.Math.Color import Color
@@ -23,7 +24,7 @@ class ConvexHullNode(SceneNode):
         self._original_parent = parent
 
         # Color of the drawn convex hull
-        self._color = Color(0.4, 0.4, 0.4, 1.0)
+        self._color = None
 
         # The y-coordinate of the convex hull mesh. Must not be 0, to prevent z-fighting.
         self._mesh_height = 0.1
@@ -72,7 +73,7 @@ class ConvexHullNode(SceneNode):
         return True
 
     def _onNodeDecoratorsChanged(self, node):
-        self._color = Color(35, 35, 35, 0.5)
+        self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
 
         convex_hull_head = self._node.callDecoration("getConvexHullHead")
         if convex_hull_head:

+ 10 - 1
cura/CuraApplication.py

@@ -234,10 +234,14 @@ class CuraApplication(QtApplication):
         Preferences.getInstance().addPreference("cura/categories_expanded", "")
         Preferences.getInstance().addPreference("cura/jobname_prefix", True)
         Preferences.getInstance().addPreference("view/center_on_select", False)
-        Preferences.getInstance().addPreference("mesh/scale_to_fit", True)
+        Preferences.getInstance().addPreference("mesh/scale_to_fit", False)
         Preferences.getInstance().addPreference("mesh/scale_tiny_meshes", True)
         Preferences.getInstance().addPreference("cura/dialog_on_project_save", True)
         Preferences.getInstance().addPreference("cura/asked_dialog_on_project_save", False)
+
+        Preferences.getInstance().addPreference("cura/currency", "€")
+        Preferences.getInstance().addPreference("cura/material_settings", "{}")
+
         for key in [
             "dialog_load_path",  # dialog_save_path is in LocalFileOutputDevicePlugin
             "dialog_profile_path",
@@ -330,6 +334,11 @@ class CuraApplication(QtApplication):
 
     showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
 
+    def setViewLegendItems(self, items):
+        self.viewLegendItemsChanged.emit(items)
+
+    viewLegendItemsChanged = pyqtSignal("QVariantList", arguments = ["items"])
+
     ##  Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
     #
     #   Note that the AutoSave plugin also calls this method.

+ 24 - 15
cura/LayerPolygon.py

@@ -1,4 +1,5 @@
 from UM.Math.Color import Color
+from UM.Application import Application
 
 import numpy
 
@@ -37,7 +38,7 @@ class LayerPolygon:
 
         # Buffering the colors shouldn't be necessary as it is not 
         # re-used and can save alot of memory usage.
-        self._color_map = self.__color_map * [1, 1, 1, self._extruder] # The alpha component is used to store the extruder nr
+        self._color_map = LayerPolygon.getColorMap() * [1, 1, 1, self._extruder] # The alpha component is used to store the extruder nr
         self._colors = self._color_map[self._types]
         
         # When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
@@ -172,17 +173,25 @@ class LayerPolygon:
 
         return normals
 
-    # Should be generated in better way, not hardcoded.
-    __color_map = numpy.array([
-        [1.0,  1.0,  1.0, 1.0], # NoneType
-        [1.0,  0.0,  0.0, 1.0], # Inset0Type
-        [0.0,  1.0,  0.0, 1.0], # InsetXType
-        [1.0,  1.0,  0.0, 1.0], # SkinType
-        [0.0,  1.0,  1.0, 1.0], # SupportType
-        [0.0,  1.0,  1.0, 1.0], # SkirtType
-        [1.0,  0.75, 0.0, 1.0], # InfillType
-        [0.0,  1.0,  1.0, 1.0], # SupportInfillType
-        [0.0,  0.0,  1.0, 1.0], # MoveCombingType
-        [0.5,  0.5,  1.0, 1.0], # MoveRetractionType
-        [0.25, 0.75, 1.0, 1.0]  # SupportInterfaceType
-    ])
+    __color_map = None
+
+    ##  Gets the instance of the VersionUpgradeManager, or creates one.
+    @classmethod
+    def getColorMap(cls):
+        if cls.__color_map is None:
+            theme = Application.getInstance().getTheme()
+            cls.__color_map = numpy.array([
+                theme.getColor("layerview_none").getRgbF(), # NoneType
+                theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type
+                theme.getColor("layerview_inset_x").getRgbF(), # InsetXType
+                theme.getColor("layerview_skin").getRgbF(), # SkinType
+                theme.getColor("layerview_support").getRgbF(), # SupportType
+                theme.getColor("layerview_skirt").getRgbF(), # SkirtType
+                theme.getColor("layerview_infill").getRgbF(), # InfillType
+                theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
+                theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
+                theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
+                theme.getColor("layerview_support_interface").getRgbF()  # SupportInterfaceType
+            ])
+
+        return cls.__color_map

+ 66 - 2
cura/PrintInformation.py

@@ -7,12 +7,14 @@ from UM.FlameProfiler import pyqtSlot
 from UM.Application import Application
 from UM.Qt.Duration import Duration
 from UM.Preferences import Preferences
+from UM.Settings import ContainerRegistry
 
 from cura.Settings.ExtruderManager import ExtruderManager
 
 import math
 import os.path
 import unicodedata
+import json
 
 from UM.i18n import i18nCatalog
 catalog = i18nCatalog("cura")
@@ -52,6 +54,7 @@ class PrintInformation(QObject):
 
         self._material_lengths = []
         self._material_weights = []
+        self._material_costs = []
 
         self._pre_sliced = False
 
@@ -65,6 +68,12 @@ class PrintInformation(QObject):
         Application.getInstance().globalContainerStackChanged.connect(self._setAbbreviatedMachineName)
         Application.getInstance().fileLoaded.connect(self.setJobName)
 
+        Preferences.getInstance().preferenceChanged.connect(self._onPreferencesChanged)
+
+        self._active_material_container = None
+        Application.getInstance().getMachineManager().activeMaterialChanged.connect(self._onActiveMaterialChanged)
+        self._onActiveMaterialChanged()
+
     currentPrintTimeChanged = pyqtSignal()
 
     preSlicedChanged = pyqtSignal()
@@ -93,28 +102,83 @@ class PrintInformation(QObject):
     def materialWeights(self):
         return self._material_weights
 
+    materialCostsChanged = pyqtSignal()
+
+    @pyqtProperty("QVariantList", notify = materialCostsChanged)
+    def materialCosts(self):
+        return self._material_costs
+
     def _onPrintDurationMessage(self, total_time, material_amounts):
         self._current_print_time.setDuration(total_time)
         self.currentPrintTimeChanged.emit()
 
+        self._material_amounts = material_amounts
+        self._calculateInformation()
+
+    def _calculateInformation(self):
         # Material amount is sent as an amount of mm^3, so calculate length from that
         r = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
         self._material_lengths = []
         self._material_weights = []
+        self._material_costs = []
+
+        material_preference_values = json.loads(Preferences.getInstance().getValue("cura/material_settings"))
+
         extruder_stacks = list(ExtruderManager.getInstance().getMachineExtruders(Application.getInstance().getGlobalContainerStack().getId()))
-        for index, amount in enumerate(material_amounts):
+        for index, amount in enumerate(self._material_amounts):
             ## Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
             #  list comprehension filtering to solve this for us.
+            material = None
             if extruder_stacks:  # Multi extrusion machine
                 extruder_stack = [extruder for extruder in extruder_stacks if extruder.getMetaDataEntry("position") == str(index)][0]
                 density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0)
+                material = extruder_stack.findContainer({"type": "material"})
             else:  # Machine with no extruder stacks
                 density = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("properties", {}).get("density", 0)
+                material = Application.getInstance().getGlobalContainerStack().findContainer({"type": "material"})
+
+            weight = float(amount) * float(density) / 1000
+            cost = 0
+            if material:
+                material_guid = material.getMetaDataEntry("GUID")
+                if material_guid in material_preference_values:
+                    material_values = material_preference_values[material_guid]
+
+                    weight_per_spool = float(material_values["spool_weight"] if material_values and "spool_weight" in material_values else 0)
+                    cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0)
 
-            self._material_weights.append(float(amount) * float(density) / 1000)
+                    if weight_per_spool != 0:
+                        cost = cost_per_spool * weight / weight_per_spool
+                    else:
+                        cost = 0
+
+            self._material_weights.append(weight)
             self._material_lengths.append(round((amount / (math.pi * r ** 2)) / 1000, 2))
+            self._material_costs.append(cost)
+
         self.materialLengthsChanged.emit()
         self.materialWeightsChanged.emit()
+        self.materialCostsChanged.emit()
+
+    def _onPreferencesChanged(self, preference):
+        if preference != "cura/material_settings":
+            return
+
+        self._calculateInformation()
+
+    def _onActiveMaterialChanged(self):
+        if self._active_material_container:
+            self._active_material_container.metaDataChanged.disconnect(self._onMaterialMetaDataChanged)
+
+        active_material_id = Application.getInstance().getMachineManager().activeMaterialId
+        active_material_containers = ContainerRegistry.getInstance().findInstanceContainers(id=active_material_id)
+
+        if active_material_containers:
+            self._active_material_container = active_material_containers[0]
+            self._active_material_container.metaDataChanged.connect(self._onMaterialMetaDataChanged)
+
+    def _onMaterialMetaDataChanged(self):
+        self._calculateInformation()
 
     @pyqtSlot(str)
     def setJobName(self, name):

+ 2 - 0
docs/How_to_use_the_flame_graph_profiler.md

@@ -22,6 +22,8 @@ To open the profiler go to the Extensions menu and select "Start BFG" from the "
 
 The time scale is at the top of the window. The blocks should be read as meaning the blocks at the bottom call the blocks which are stacked on top of them. Hover the mouse to get more detailed information about a block such as the name of the code involved and its duration. Use the zoom buttons or mouse wheel to zoom in. The display can be panned by dragging with the left mouse button.
 
+Note: The profiler front-end itself is quite "heavy" (ok, not optimised). It runs much better in Google Chrome or Chromium than Firefox. It is also a good idea to keep recording sessions short for the same reason.
+
 
 What the Profiler Sees
 ----------------------

+ 1 - 1
plugins/3MFReader/ThreeMFReader.py

@@ -119,7 +119,7 @@ class ThreeMFReader(MeshReader):
             Job.yieldThread()
 
         # TODO: We currently do not check for normals and simply recalculate them.
-        mesh_builder.calculateNormals()
+        mesh_builder.calculateNormals(fast=True)
         mesh_builder.setFileName(name)
         mesh_data = mesh_builder.build()
 

+ 36 - 24
plugins/3MFReader/WorkspaceDialog.py

@@ -70,8 +70,9 @@ class WorkspaceDialog(QObject):
         return self._variant_type
 
     def setVariantType(self, variant_type):
-        self._variant_type = variant_type
-        self.variantTypeChanged.emit()
+        if self._variant_type != variant_type:
+            self._variant_type = variant_type
+            self.variantTypeChanged.emit()
 
     @pyqtProperty(str, notify=machineTypeChanged)
     def machineType(self):
@@ -82,8 +83,9 @@ class WorkspaceDialog(QObject):
         self.machineTypeChanged.emit()
 
     def setNumUserSettings(self, num_user_settings):
-        self._num_user_settings = num_user_settings
-        self.numVisibleSettingsChanged.emit()
+        if self._num_user_settings != num_user_settings:
+            self._num_user_settings = num_user_settings
+            self.numVisibleSettingsChanged.emit()
 
     @pyqtProperty(int, notify=numUserSettingsChanged)
     def numUserSettings(self):
@@ -94,40 +96,45 @@ class WorkspaceDialog(QObject):
         return self._objects_on_plate
 
     def setHasObjectsOnPlate(self, objects_on_plate):
-        self._objects_on_plate = objects_on_plate
-        self.objectsOnPlateChanged.emit()
+        if self._objects_on_plate != objects_on_plate:
+            self._objects_on_plate = objects_on_plate
+            self.objectsOnPlateChanged.emit()
 
     @pyqtProperty("QVariantList", notify = materialLabelsChanged)
     def materialLabels(self):
         return self._material_labels
 
     def setMaterialLabels(self, material_labels):
-        self._material_labels = material_labels
-        self.materialLabelsChanged.emit()
+        if self._material_labels != material_labels:
+            self._material_labels = material_labels
+            self.materialLabelsChanged.emit()
 
     @pyqtProperty("QVariantList", notify=extrudersChanged)
     def extruders(self):
         return self._extruders
 
     def setExtruders(self, extruders):
-        self._extruders = extruders
-        self.extrudersChanged.emit()
+        if self._extruders != extruders:
+            self._extruders = extruders
+            self.extrudersChanged.emit()
 
     @pyqtProperty(str, notify = machineNameChanged)
     def machineName(self):
         return self._machine_name
 
     def setMachineName(self, machine_name):
-        self._machine_name = machine_name
-        self.machineNameChanged.emit()
+        if self._machine_name != machine_name:
+            self._machine_name = machine_name
+            self.machineNameChanged.emit()
 
     @pyqtProperty(str, notify=qualityTypeChanged)
     def qualityType(self):
         return self._quality_type
 
     def setQualityType(self, quality_type):
-        self._quality_type = quality_type
-        self.qualityTypeChanged.emit()
+        if self._quality_type != quality_type:
+            self._quality_type = quality_type
+            self.qualityTypeChanged.emit()
 
     @pyqtProperty(int, notify=numSettingsOverridenByQualityChangesChanged)
     def numSettingsOverridenByQualityChanges(self):
@@ -142,8 +149,9 @@ class WorkspaceDialog(QObject):
         return self._quality_name
 
     def setQualityName(self, quality_name):
-        self._quality_name = quality_name
-        self.qualityNameChanged.emit()
+        if self._quality_name != quality_name:
+            self._quality_name = quality_name
+            self.qualityNameChanged.emit()
 
     @pyqtProperty(str, notify=activeModeChanged)
     def activeMode(self):
@@ -165,8 +173,9 @@ class WorkspaceDialog(QObject):
         return self._num_visible_settings
 
     def setNumVisibleSettings(self, num_visible_settings):
-        self._num_visible_settings = num_visible_settings
-        self.numVisibleSettingsChanged.emit()
+        if self._num_visible_settings != num_visible_settings:
+            self._num_visible_settings = num_visible_settings
+            self.numVisibleSettingsChanged.emit()
 
     @pyqtProperty(bool, notify = machineConflictChanged)
     def machineConflict(self):
@@ -191,16 +200,19 @@ class WorkspaceDialog(QObject):
         Application.getInstance().getBackend().close()
 
     def setMaterialConflict(self, material_conflict):
-        self._has_material_conflict = material_conflict
-        self.materialConflictChanged.emit()
+        if self._has_material_conflict != material_conflict:
+            self._has_material_conflict = material_conflict
+            self.materialConflictChanged.emit()
 
     def setMachineConflict(self, machine_conflict):
-        self._has_machine_conflict = machine_conflict
-        self.machineConflictChanged.emit()
+        if self._has_machine_conflict != machine_conflict:
+            self._has_machine_conflict = machine_conflict
+            self.machineConflictChanged.emit()
 
     def setQualityChangesConflict(self, quality_changes_conflict):
-        self._has_quality_changes_conflict = quality_changes_conflict
-        self.qualityChangesConflictChanged.emit()
+        if self._has_quality_changes_conflict != quality_changes_conflict:
+            self._has_quality_changes_conflict = quality_changes_conflict
+            self.qualityChangesConflictChanged.emit()
 
     def getResult(self):
         if "machine" in self._result and not self._has_machine_conflict:

+ 5 - 4
plugins/3MFWriter/ThreeMFWriter.py

@@ -46,7 +46,7 @@ class ThreeMFWriter(MeshWriter):
         result += str(matrix._data[2,2]) + " "
         result += str(matrix._data[0,3]) + " "
         result += str(matrix._data[1,3]) + " "
-        result += str(matrix._data[2,3]) + " "
+        result += str(matrix._data[2,3])
         return result
 
     ##  Should we store the archive
@@ -80,10 +80,11 @@ class ThreeMFWriter(MeshWriter):
             model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
 
             model = ET.Element("model", unit = "millimeter", xmlns = self._namespaces["3mf"])
+            model.set("xmlns:cura", self._namespaces["cura"])
 
-            # Add the version of Cura this was created with. As "CuraVersion" is not a recognised metadata name
-            #  by 3mf itself, we place it in our own namespace.
-            version_metadata = ET.SubElement(model, "metadata", xmlns = self._namespaces["cura"], name = "CuraVersion")
+            # Add the version of Cura this was created with. Since there is no "version" or similar metadata name we need
+            # to prefix it with the cura namespace, as specified by the 3MF specification.
+            version_metadata = ET.SubElement(model, "metadata", name = "cura:version")
             version_metadata.text = Application.getInstance().getVersion()
 
             resources = ET.SubElement(model, "resources")

+ 5 - 0
plugins/ChangeLogPlugin/ChangeLog.txt

@@ -98,6 +98,9 @@ Use a mesh to specify a volume within which to classify nothing as overhang for
 *Delta printer support
 This release adds support for printers with elliptic buildplates. This feature has not been extensively tested so please let us know if it works or get involved in improving it.
 
+*AppImage for Linux
+The Linux distribution is now in AppImage format, which makes Cura easier to install.
+
 *bugfixes
 The user is now notified when a new version of Cura is available.
 When searching in the setting visibility preferences, the category for each setting is always displayed.
@@ -122,6 +125,8 @@ There are no more unnecessary retractions in support.
 Each layer now has less extruder switches than the machine has extruders.
 Concentric infill doesn’t generate the first infill perimeter next to the walls.
 Extruder priming now always happens on the first layer.
+Raising the build plate of the Ultimaker 2 now has the proper speed again.
+Changing material while the Ultimaker 2 is paused works again.
 
 [2.3.1]
 *Layer Height in Profile Selection

Some files were not shown because too many files changed in this diff