diff --git a/.gitignore b/.gitignore
index 9c9d57f175..03ae94a87f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,7 @@ resources/i18n/en_7S
resources/i18n/x-test
resources/firmware
resources/materials
+CuraEngine.exe
LC_MESSAGES
.cache
*.qmlc
diff --git a/cura/CuraSplashScreen.py b/cura/CuraSplashScreen.py
index dd4dd9b2cc..4e4b2ba3a3 100644
--- a/cura/CuraSplashScreen.py
+++ b/cura/CuraSplashScreen.py
@@ -2,10 +2,9 @@
# Uranium is released under the terms of the AGPLv3 or higher.
from threading import Thread, Event
-import time
-from PyQt5.QtCore import Qt, QCoreApplication
-from PyQt5.QtGui import QPixmap, QColor, QFont, QFontMetrics, QImage, QPen
+from PyQt5.QtCore import Qt
+from PyQt5.QtGui import QPixmap, QColor, QFont, QPen, QPainter
from PyQt5.QtWidgets import QSplashScreen
from UM.Resources import Resources
@@ -22,8 +21,6 @@ class CuraSplashScreen(QSplashScreen):
self._current_message = ""
- self._loading_image = QImage(Resources.getPath(Resources.Images, "loading.png"))
- self._loading_image = self._loading_image.scaled(30, 30, Qt.KeepAspectRatio)
self._loading_image_rotation_angle = 0
self._to_stop = False
@@ -46,6 +43,8 @@ class CuraSplashScreen(QSplashScreen):
painter.save()
painter.setPen(QColor(255, 255, 255, 255))
+ painter.setRenderHint(QPainter.Antialiasing)
+ painter.setRenderHint(QPainter.Antialiasing, True)
version = Application.getInstance().getVersion().split("-")
buildtype = Application.getInstance().getBuildType()
@@ -54,27 +53,33 @@ class CuraSplashScreen(QSplashScreen):
# draw version text
font = QFont() # Using system-default font here
- font.setPointSize(34)
+ font.setPointSize(28)
painter.setFont(font)
- painter.drawText(275, 87, 330 * self._scale, 230 * self._scale, Qt.AlignLeft | Qt.AlignBottom, version[0])
+ painter.drawText(220, 66, 330 * self._scale, 230 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[0])
if len(version) > 1:
font.setPointSize(12)
painter.setFont(font)
- painter.drawText(320, 82, 330 * self._scale, 255 * self._scale, Qt.AlignLeft | Qt.AlignBottom, version[1])
+ painter.setPen(QColor(200, 200, 200, 255))
+ painter.drawText(252, 105, 330 * self._scale, 255 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[1])
+ painter.setPen(QColor(255, 255, 255, 255))
# draw the loading image
pen = QPen()
- pen.setWidth(4 * self._scale)
- pen.setColor(QColor(255, 255, 255, 255))
+ pen.setWidth(6 * self._scale)
+ pen.setColor(QColor(32, 166, 219, 255))
painter.setPen(pen)
- painter.drawArc(130, 380, 32 * self._scale, 32 * self._scale, self._loading_image_rotation_angle * 16, 300 * 16)
+ painter.drawArc(60, 150, 32 * self._scale, 32 * self._scale, self._loading_image_rotation_angle * 16, 300 * 16)
# draw message text
if self._current_message:
font = QFont() # Using system-default font here
- font.setPointSize(16)
+ font.setPointSize(10)
+ pen = QPen()
+ pen.setColor(QColor(255, 255, 255, 255))
+ painter.setPen(pen)
painter.setFont(font)
- painter.drawText(180, 243, 330 * self._scale, 230 * self._scale, Qt.AlignLeft | Qt.AlignBottom,
+ painter.drawText(100, 128, 170, 64,
+ Qt.AlignLeft | Qt.AlignVCenter | Qt.TextWordWrap,
self._current_message)
painter.restore()
@@ -86,7 +91,6 @@ class CuraSplashScreen(QSplashScreen):
self._current_message = message
self.messageChanged.emit(message)
- self.repaint()
def close(self):
# set stop flags
diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py
index b00c5a632c..dc5594dc7b 100755
--- a/cura/PlatformPhysics.py
+++ b/cura/PlatformPhysics.py
@@ -67,10 +67,18 @@ class PlatformPhysics:
# Move it downwards if bottom is above platform
move_vector = Vector()
- if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")) and node.isEnabled(): #If an object is grouped, don't move it down
+
+ # Check if this is the first time a project file node was loaded (disable auto drop in that case), defaults to True
+ should_auto_drop = node.getSetting("auto_drop", True)
+
+ # If a node is grouped or it's loaded from a project file (auto-drop disabled), don't move it down
+ if Preferences.getInstance().getValue("physics/automatic_drop_down") and not (node.getParent() and node.getParent().callDecoration("isGroup")) and node.isEnabled() and should_auto_drop:
z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
move_vector = move_vector.set(y=-bbox.bottom + z_offset)
+ # Enable auto-drop after processing the project file node for the first time
+ node.setSetting("auto_drop", False)
+
# If there is no convex hull for the node, start calculating it and continue.
if not node.getDecorator(ConvexHullDecorator):
node.addDecorator(ConvexHullDecorator())
diff --git a/cura/QualityManager.py b/cura/QualityManager.py
index e92829e546..813d23ea13 100644
--- a/cura/QualityManager.py
+++ b/cura/QualityManager.py
@@ -82,6 +82,17 @@ class QualityManager:
return list(common_quality_types)
+ def findAllQualitiesForMachineAndMaterials(self, machine_definition: "DefinitionContainerInterface", material_containers: List[InstanceContainer]) -> List[InstanceContainer]:
+ # Determine the common set of quality types which can be
+ # applied to all of the materials for this machine.
+ quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_containers[0])
+ qualities = set(quality_type_dict.values())
+ for material_container in material_containers[1:]:
+ next_quality_type_dict = self.__fetchQualityTypeDictForMaterial(machine_definition, material_container)
+ qualities.update(set(next_quality_type_dict.values()))
+
+ return list(qualities)
+
## Fetches a dict of quality types names to quality profiles for a combination of machine and material.
#
# \param machine_definition \type{DefinitionContainer} the machine definition.
@@ -121,7 +132,7 @@ class QualityManager:
# \param material_container \type{InstanceContainer} the material.
# \return \type{List[InstanceContainer]} the list of suitable qualities.
def findAllQualitiesForMachineMaterial(self, machine_definition: "DefinitionContainerInterface", material_container: InstanceContainer) -> List[InstanceContainer]:
- criteria = {"type": "quality" }
+ criteria = {"type": "quality"}
result = self._getFilteredContainersForStack(machine_definition, [material_container], **criteria)
if not result:
basic_materials = self._getBasicMaterials(material_container)
diff --git a/cura/Settings/ProfilesModel.py b/cura/Settings/ProfilesModel.py
index 545f44401a..2942577fc6 100644
--- a/cura/Settings/ProfilesModel.py
+++ b/cura/Settings/ProfilesModel.py
@@ -1,6 +1,8 @@
-# Copyright (c) 2016 Ultimaker B.V.
+# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the AGPLv3 or higher.
+from collections import OrderedDict
+
from PyQt5.QtCore import Qt
from UM.Application import Application
@@ -10,14 +12,19 @@ from UM.Settings.Models.InstanceContainersModel import InstanceContainersModel
from cura.QualityManager import QualityManager
from cura.Settings.ExtruderManager import ExtruderManager
+
## QML Model for listing the current list of valid quality profiles.
#
class ProfilesModel(InstanceContainersModel):
LayerHeightRole = Qt.UserRole + 1001
+ LayerHeightWithoutUnitRole = Qt.UserRole + 1002
+ AvailableRole = Qt.UserRole + 1003
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.LayerHeightRole, "layer_height")
+ self.addRoleName(self.LayerHeightWithoutUnitRole, "layer_height_without_unit")
+ self.addRoleName(self.AvailableRole, "available")
Application.getInstance().globalContainerStackChanged.connect(self._update)
@@ -47,8 +54,9 @@ class ProfilesModel(InstanceContainersModel):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack is None:
return []
+ global_stack_definition = global_container_stack.getBottom()
- # Get the list of extruders and place the selected extruder at the front of the list.
+ # Get the list of extruders and place the selected extruder at the front of the list.
extruder_manager = ExtruderManager.getInstance()
active_extruder = extruder_manager.getActiveExtruderStack()
extruder_stacks = extruder_manager.getActiveExtruderStacks()
@@ -56,10 +64,22 @@ class ProfilesModel(InstanceContainersModel):
extruder_stacks.remove(active_extruder)
extruder_stacks = [active_extruder] + extruder_stacks
- # Fetch the list of useable qualities across all extruders.
+ if ExtruderManager.getInstance().getActiveExtruderStacks():
+ # Multi-extruder machine detected.
+ materials = [extruder.material for extruder in extruder_stacks]
+ else:
+ # Machine with one extruder.
+ materials = [global_container_stack.material]
+
+ # Fetch the list of usable qualities across all extruders.
# The actual list of quality profiles come from the first extruder in the extruder list.
- return QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack,
- extruder_stacks)
+ result = QualityManager.getInstance().findAllQualitiesForMachineAndMaterials(global_stack_definition,
+ materials)
+ for quality in QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(
+ global_container_stack, extruder_stacks):
+ if quality not in result:
+ result.append(quality)
+ return result
## Re-computes the items in this model, and adds the layer height role.
def _recomputeItems(self):
@@ -67,6 +87,17 @@ class ProfilesModel(InstanceContainersModel):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack is None:
return
+
+ # Get the list of extruders and place the selected extruder at the front of the list.
+ extruder_manager = ExtruderManager.getInstance()
+ active_extruder = extruder_manager.getActiveExtruderStack()
+ extruder_stacks = extruder_manager.getActiveExtruderStacks()
+ if active_extruder in extruder_stacks:
+ extruder_stacks.remove(active_extruder)
+ extruder_stacks = [active_extruder] + extruder_stacks
+ # Get a list of available qualities for this machine and material
+ qualities = QualityManager.getInstance().findAllUsableQualitiesForMachineAndExtruders(global_container_stack,
+ extruder_stacks)
container_registry = ContainerRegistry.getInstance()
machine_manager = Application.getInstance().getMachineManager()
@@ -74,17 +105,44 @@ class ProfilesModel(InstanceContainersModel):
if not unit:
unit = ""
+ # group all quality items according to quality_types, so we know which profile suits the currently
+ # active machine and material, and later yield the right ones.
+ tmp_all_quality_items = OrderedDict()
for item in super()._recomputeItems():
+ profile = container_registry.findContainers(id = item["id"])
+ quality_type = profile[0].getMetaDataEntry("quality_type") if profile else ""
+
+ if quality_type not in tmp_all_quality_items:
+ tmp_all_quality_items[quality_type] = {"suitable_container": None,
+ "all_containers": []}
+
+ tmp_all_quality_items[quality_type]["all_containers"].append(item)
+ if tmp_all_quality_items[quality_type]["suitable_container"] is None and profile[0] in qualities:
+ tmp_all_quality_items[quality_type]["suitable_container"] = item
+
+ # reverse the ordering (finest first, coarsest last)
+ all_quality_items = OrderedDict()
+ for key in reversed(tmp_all_quality_items.keys()):
+ all_quality_items[key] = tmp_all_quality_items[key]
+
+ for data_item in all_quality_items.values():
+ item = data_item["suitable_container"]
+ if item is None:
+ item = data_item["all_containers"][0]
+
profile = container_registry.findContainers(id = item["id"])
if not profile:
item["layer_height"] = "" #Can't update a profile that is unknown.
+ item["available"] = False
yield item
continue
- #Easy case: This profile defines its own layer height.
profile = profile[0]
+ item["available"] = profile in qualities
+
+ #Easy case: This profile defines its own layer height.
if profile.hasProperty("layer_height", "value"):
- item["layer_height"] = str(profile.getProperty("layer_height", "value")) + unit
+ self._setItemLayerHeight(item, profile.getProperty("layer_height", "value"), unit)
yield item
continue
@@ -102,7 +160,7 @@ class ProfilesModel(InstanceContainersModel):
else:
quality = None
if quality and quality.hasProperty("layer_height", "value"):
- item["layer_height"] = str(quality.getProperty("layer_height", "value")) + unit
+ self._setItemLayerHeight(item, quality.getProperty("layer_height", "value"), unit)
yield item
continue
@@ -112,5 +170,9 @@ class ProfilesModel(InstanceContainersModel):
skip_until_container = global_container_stack.variant
if not skip_until_container or skip_until_container == ContainerRegistry.getInstance().getEmptyInstanceContainer(): #No variant in stack.
skip_until_container = global_container_stack.getBottom()
- item["layer_height"] = str(global_container_stack.getRawProperty("layer_height", "value", skip_until_container = skip_until_container.getId())) + unit #Fall through to the currently loaded material.
+ self._setItemLayerHeight(item, global_container_stack.getRawProperty("layer_height", "value", skip_until_container = skip_until_container.getId()), unit) # Fall through to the currently loaded material.
yield item
+
+ def _setItemLayerHeight(self, item, value, unit):
+ item["layer_height"] = str(value) + unit
+ item["layer_height_without_unit"] = str(value)
diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py
index d473ecaa8b..66dfb6130e 100755
--- a/plugins/3MFReader/ThreeMFReader.py
+++ b/plugins/3MFReader/ThreeMFReader.py
@@ -73,11 +73,11 @@ class ThreeMFReader(MeshReader):
return temp_mat
-
- ## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium scenenode.
- # \returns Uranium Scenen node.
+ ## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium SceneNode.
+ # \returns Uranium SceneNode.
def _convertSavitarNodeToUMNode(self, savitar_node):
um_node = SceneNode()
+ um_node.setSetting("auto_drop", False) # Disable the auto-drop feature when loading a project file and processing the nodes for the first time
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
um_node.setTransformation(transformation)
mesh_builder = MeshBuilder()
diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py
new file mode 100644
index 0000000000..1c099705b1
--- /dev/null
+++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py
@@ -0,0 +1,49 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the AGPLv3 or higher.
+
+from UM.Extension import Extension
+from UM.Preferences import Preferences
+from UM.Logger import Logger
+from UM.i18n import i18nCatalog
+from cura.Settings.GlobalStack import GlobalStack
+
+from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob
+from UM.Settings.ContainerRegistry import ContainerRegistry
+
+i18n_catalog = i18nCatalog("cura")
+
+
+## This Extension checks for new versions of the firmware based on the latest checked version number.
+# The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
+# to change it to work for other applications.
+class FirmwareUpdateChecker(Extension):
+ JEDI_VERSION_URL = "http://software.ultimaker.com/jedi/releases/latest.version"
+
+ def __init__(self):
+ super().__init__()
+
+ # Initialize the Preference called `latest_checked_firmware` that stores the last version
+ # checked for the UM3. In the future if we need to check other printers' firmware
+ Preferences.getInstance().addPreference("info/latest_checked_firmware", "")
+
+ # Listen to a Signal that indicates a change in the list of printers, just if the user has enabled the
+ # 'check for updates' option
+ Preferences.getInstance().addPreference("info/automatic_update_check", True)
+ if Preferences.getInstance().getValue("info/automatic_update_check"):
+ ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
+
+ def _onContainerAdded(self, container):
+ # Only take care when a new GlobalStack was added
+ if isinstance(container, GlobalStack):
+ Logger.log("i", "You have a '%s' in printer list. Let's check the firmware!", container.getId())
+ self.checkFirmwareVersion(container, True)
+
+ ## Connect with software.ultimaker.com, load latest.version and check version info.
+ # If the version info is different from the current version, spawn a message to
+ # allow the user to download it.
+ #
+ # \param silent type(boolean) Suppresses messages other than "new version found" messages.
+ # This is used when checking for a new firmware version at startup.
+ def checkFirmwareVersion(self, container = None, silent = False):
+ job = FirmwareUpdateCheckerJob(container = container, silent = silent, url = self.JEDI_VERSION_URL)
+ job.start()
diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py
new file mode 100644
index 0000000000..108cfa4c0d
--- /dev/null
+++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py
@@ -0,0 +1,86 @@
+# Copyright (c) 2017 Ultimaker B.V.
+# Cura is released under the terms of the AGPLv3 or higher.
+
+from UM.Preferences import Preferences
+from UM.Application import Application
+from UM.Message import Message
+from UM.Logger import Logger
+from UM.Job import Job
+
+import urllib.request
+import codecs
+
+from PyQt5.QtCore import QUrl
+from PyQt5.QtGui import QDesktopServices
+
+from UM.i18n import i18nCatalog
+i18n_catalog = i18nCatalog("cura")
+
+
+## This job checks if there is an update available on the provided URL.
+class FirmwareUpdateCheckerJob(Job):
+ def __init__(self, container = None, silent = False, url = None):
+ super().__init__()
+ self._container = container
+ self.silent = silent
+ self._url = url
+ self._download_url = None # If an update was found, the download_url will be set to the location of the new version.
+
+ ## Callback for the message that is spawned when there is a new version.
+ def actionTriggered(self, message, action):
+ if action == "download":
+ if self._download_url is not None:
+ QDesktopServices.openUrl(QUrl(self._download_url))
+
+ def run(self):
+ self._download_url = None # Reset download ur.
+ if not self._url:
+ Logger.log("e", "Can not check for a new release. URL not set!")
+ return
+
+ try:
+ request = urllib.request.Request(self._url)
+ current_version_file = urllib.request.urlopen(request)
+ reader = codecs.getreader("utf-8")
+
+ # get machine name from the definition container
+ machine_name = self._container.definition.getName()
+ machine_name_parts = machine_name.lower().split(" ")
+
+ # If it is not None, then we compare between the checked_version and the current_version
+ # Now we just do that if the active printer is Ultimaker 3 or Ultimaker 3 Extended or any
+ # other Ultimaker 3 that will come in the future
+ if len(machine_name_parts) >= 2 and machine_name_parts[:2] == ["ultimaker", "3"]:
+ # Nothing to parse, just get the string
+ # TODO: In the future may be done by parsing a JSON file with diferent version for each printer model
+ current_version = reader(current_version_file).readline().rstrip()
+ Logger.log("i", "Reading firmware version of %s: %s", machine_name, current_version)
+
+ # If it is the first time the version is checked, the checked_version is None
+ checked_version = Preferences.getInstance().getValue("info/latest_checked_firmware")
+
+ # If the checked_version is '', it's because is the first time we check firmware and in this case
+ # we will not show the notification, but we will store it for the next time
+ if (checked_version != "") and (checked_version != current_version):
+ message = Message(i18n_catalog.i18nc("@info", "New %s firmware available