Merge pull request #1 from Ultimaker/master

Update to master
This commit is contained in:
PurpleHullPeas 2020-04-04 22:42:50 -05:00 committed by GitHub
commit a1a7d48e46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2119 changed files with 117339 additions and 72817 deletions

View File

@ -6,6 +6,7 @@ on:
- master - master
- 'WIP**' - 'WIP**'
- '4.*' - '4.*'
- 'CURA-*'
pull_request: pull_request:
jobs: jobs:
build: build:

1
.gitignore vendored
View File

@ -53,6 +53,7 @@ plugins/GodMode
plugins/OctoPrintPlugin plugins/OctoPrintPlugin
plugins/ProfileFlattener plugins/ProfileFlattener
plugins/SettingsGuide plugins/SettingsGuide
plugins/SVGToolpathReader
plugins/X3GWriter plugins/X3GWriter
#Build stuff #Build stuff

116
.pylintrc Normal file
View File

@ -0,0 +1,116 @@
# Copyright (c) 2019 Ultimaker B.V.
# This file contains the Pylint rules used in the stardust projects.
# To configure PyLint as an external tool in PyCharm, create a new External Tool with the settings:
#
# Name: PyLint
# Program: Check with 'which pylint'. For example: ~/.local/bin/pylint
# Arguments: $FileDirName$ --rcfile=.pylintrc --msg-template='{abspath}:{line}:{column}:({symbol}):{msg_id}:{msg}'
# Working directory: $ContentRoot$
# Output filters: $FILE_PATH$:$LINE$:$COLUMN$:.*
#
# You can add a keyboard shortcut in the keymap settings. To run Pylint to a project, select the module
# you want to check (e.g. cura folder) before running the external tool.
#
# If you find a better way to configure the external tool please edit this file.
[MASTER]
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=pylint_quotes
# We expect double string quotes
string-quote=double-avoid-escape
# When enabled, pylint would attempt to guess common misconfiguration and emit
# user-friendly hints instead of false-positive error messages.
suggestion-mode=yes
# Add files or directories to the blacklist. They should be base names, not paths.
ignore=tests
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
[MESSAGES CONTROL]
# C0326: No space allowed around keyword argument assignment
# C0411: Ignore import order because the rules are different than in PyCharm, so automatic imports break lots of builds
# C0412: Ignore import order because the rules are different than in PyCharm, so automatic imports break lots of builds
# C0413: Ignore import order because the rules are different than in PyCharm, so automatic imports break lots of builds
# R0201: Method could be a function (no-self-use)
# R0401: Cyclic imports (cyclic-import) are used for typing
# R0801: Unfortunately the error is triggered for a lot of similar models (duplicate-code)
# R1710: Either all return statements in a function should return an expression, or none of them should.
# W0221: Parameters differ from overridden method (tornado http methods have a flexible number of parameters)
# W0511: Ignore warnings generated for TODOs in the code
# C0111: We don't use docstring
# C0303: Trailing whitespace isn't something we care about
# C4001: You can put " in a string if you escape it first...
disable=C0326,C0411,C0412,C0413,R0201,R0401,R0801,R1710,W0221,W0511, C0111, C0303,C4001
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=120
# Maximum number of lines in a module.
max-module-lines=500
good-names=os
[BASIC]
# allow modules and functions to use PascalCase
module-rgx=[a-zA-Z0-9_]+$
function-rgx=
## Allowed methods:
# getSomething
# _getSomething
# __getSomething
# __new__
## Disallowed:
# _GET
# GetSomething
method-rgx=(_{,2}[a-z][A-Za-z0-9]*_{,2})$
[DESIGN]
# Maximum number of arguments for function / method.
max-args=7
# Maximum number of attributes for a class (see R0902).
max-attributes=8
# Maximum number of boolean expressions in an if statement.
max-bool-expr=5
# Maximum number of branch for function / method body.
max-branches=12
# Maximum number of locals for function / method body.
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body.
max-returns=6
# Maximum number of statements in function / method body.
max-statements=50
# Minimum number of public methods for a class (R0903).
# We set this to 0 because our models and fields do not have methods.
min-public-methods=0
ignored-argument-names=arg|args|kwargs|_
[CLASSES]
defining-attr-methods=__init__,__new__,setUp,initialize
[TYPECHECK]
ignored-classes=NotImplemented
[VARIABLES]
dummy-variables-rgx=_+[a-z0-9_]{2,30}

View File

@ -5,8 +5,8 @@ include(GNUInstallDirs)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake) list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository") set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE PATH "The location of the Uranium repository")
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository") set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE PATH "The location of the scripts directory of the Uranium repository")
# Tests # Tests
include(CuraTests) include(CuraTests)
@ -23,6 +23,7 @@ set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root") set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version") set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version") set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version")
set(CURA_MARKETPLACE_ROOT "" CACHE STRING "Alternative Marketplace location")
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY) configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)

View File

@ -56,6 +56,13 @@ function(cura_add_test)
endif() endif()
endfunction() endfunction()
#Add test for import statements which are not compatible with all builds
add_test(
NAME "invalid-imports"
COMMAND ${Python3_EXECUTABLE} scripts/check_invalid_imports.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}") cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
file(GLOB_RECURSE _plugins plugins/*/__init__.py) file(GLOB_RECURSE _plugins plugins/*/__init__.py)

View File

@ -4,12 +4,11 @@ from typing import Optional, Dict, TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
from UM.i18n import i18nCatalog
from UM.Message import Message from UM.Message import Message
from cura import UltimakerCloudAuthentication from UM.i18n import i18nCatalog
from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings from cura.OAuth2.Models import OAuth2Settings
from cura.UltimakerCloud import UltimakerCloudAuthentication
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication

View File

@ -28,9 +28,10 @@ class CuraAPI(QObject):
# The main reason for this is that we want to prevent consumers of API to have a dependency on CuraApplication. # The main reason for this is that we want to prevent consumers of API to have a dependency on CuraApplication.
# Since the API is intended to be used by plugins, the cura application should have already created this. # Since the API is intended to be used by plugins, the cura application should have already created this.
def __new__(cls, application: Optional["CuraApplication"] = None): def __new__(cls, application: Optional["CuraApplication"] = None):
if cls.__instance is None: if cls.__instance is not None:
raise RuntimeError("Tried to create singleton '{class_name}' more than once.".format(class_name = CuraAPI.__name__))
if application is None: if application is None:
raise Exception("Upon first time creation, the application must be set.") raise RuntimeError("Upon first time creation, the application must be set.")
cls.__instance = super(CuraAPI, cls).__new__(cls) cls.__instance = super(CuraAPI, cls).__new__(cls)
cls._application = application cls._application = application
return cls.__instance return cls.__instance

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
# --------- # ---------
@ -13,7 +13,7 @@ DEFAULT_CURA_DEBUG_MODE = False
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for # Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the # example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
# CuraVersion.py.in template. # CuraVersion.py.in template.
CuraSDKVersion = "7.0.0" CuraSDKVersion = "7.1.0"
try: try:
from cura.CuraVersion import CuraAppName # type: ignore from cura.CuraVersion import CuraAppName # type: ignore

View File

@ -1,6 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import List from typing import List, Optional
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Logger import Logger from UM.Logger import Logger
@ -8,6 +8,7 @@ from UM.Math.Polygon import Polygon
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from cura.Arranging.ShapeArray import ShapeArray from cura.Arranging.ShapeArray import ShapeArray
from cura.BuildVolume import BuildVolume
from cura.Scene import ZOffsetDecorator from cura.Scene import ZOffsetDecorator
from collections import namedtuple from collections import namedtuple
@ -27,7 +28,7 @@ LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points
# #
# Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance. # Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance.
class Arrange: class Arrange:
build_volume = None build_volume = None # type: Optional[BuildVolume]
def __init__(self, x, y, offset_x, offset_y, scale = 0.5): def __init__(self, x, y, offset_x, offset_y, scale = 0.5):
self._scale = scale # convert input coordinates to arrange coordinates self._scale = scale # convert input coordinates to arrange coordinates
@ -68,7 +69,7 @@ class Arrange:
points = copy.deepcopy(vertices._points) points = copy.deepcopy(vertices._points)
# After scaling (like up to 0.1 mm) the node might not have points # After scaling (like up to 0.1 mm) the node might not have points
if len(points) == 0: if not points.size:
continue continue
shape_arr = ShapeArray.fromPolygon(points, scale = scale) shape_arr = ShapeArray.fromPolygon(points, scale = scale)
@ -113,7 +114,7 @@ class Arrange:
found_spot = True found_spot = True
self.place(x, y, offset_shape_arr) # place the object in arranger self.place(x, y, offset_shape_arr) # place the object in arranger
else: else:
Logger.log("d", "Could not find spot!"), Logger.log("d", "Could not find spot!")
found_spot = False found_spot = False
node.setPosition(Vector(200, center_y, 100)) node.setPosition(Vector(200, center_y, 100))
return found_spot return found_spot
@ -172,7 +173,10 @@ class Arrange:
def bestSpot(self, shape_arr, start_prio = 0, step = 1): def bestSpot(self, shape_arr, start_prio = 0, step = 1):
start_idx_list = numpy.where(self._priority_unique_values == start_prio) start_idx_list = numpy.where(self._priority_unique_values == start_prio)
if start_idx_list: if start_idx_list:
try:
start_idx = start_idx_list[0][0] start_idx = start_idx_list[0][0]
except IndexError:
start_idx = 0
else: else:
start_idx = 0 start_idx = 0
for priority in self._priority_unique_values[start_idx::step]: for priority in self._priority_unique_values[start_idx::step]:

View File

@ -29,7 +29,7 @@ class ArrangeArray:
self._has_empty = False self._has_empty = False
self._arrange = [] # type: List[Arrange] self._arrange = [] # type: List[Arrange]
def _update_first_empty(self): def _updateFirstEmpty(self):
for i, a in enumerate(self._arrange): for i, a in enumerate(self._arrange):
if a.isEmpty: if a.isEmpty:
self._first_empty = i self._first_empty = i
@ -42,7 +42,7 @@ class ArrangeArray:
new_arrange = Arrange.create(x = self._x, y = self._y, fixed_nodes = self._fixed_nodes) new_arrange = Arrange.create(x = self._x, y = self._y, fixed_nodes = self._fixed_nodes)
self._arrange.append(new_arrange) self._arrange.append(new_arrange)
self._count += 1 self._count += 1
self._update_first_empty() self._updateFirstEmpty()
def count(self): def count(self):
return self._count return self._count

View File

@ -2,12 +2,16 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
from typing import Any, TYPE_CHECKING
from UM.Logger import Logger from UM.Logger import Logger
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
class AutoSave: class AutoSave:
def __init__(self, application): def __init__(self, application: "CuraApplication") -> None:
self._application = application self._application = application
self._application.getPreferences().preferenceChanged.connect(self._triggerTimer) self._application.getPreferences().preferenceChanged.connect(self._triggerTimer)
@ -22,14 +26,14 @@ class AutoSave:
self._enabled = True self._enabled = True
self._saving = False self._saving = False
def initialize(self): def initialize(self) -> None:
# only initialise if the application is created and has started # only initialise if the application is created and has started
self._change_timer.timeout.connect(self._onTimeout) self._change_timer.timeout.connect(self._onTimeout)
self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged) self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged() self._onGlobalStackChanged()
self._triggerTimer() self._triggerTimer()
def _triggerTimer(self, *args): def _triggerTimer(self, *args: Any) -> None:
if not self._saving: if not self._saving:
self._change_timer.start() self._change_timer.start()
@ -40,7 +44,7 @@ class AutoSave:
else: else:
self._change_timer.stop() self._change_timer.stop()
def _onGlobalStackChanged(self): def _onGlobalStackChanged(self) -> None:
if self._global_stack: if self._global_stack:
self._global_stack.propertyChanged.disconnect(self._triggerTimer) self._global_stack.propertyChanged.disconnect(self._triggerTimer)
self._global_stack.containersChanged.disconnect(self._triggerTimer) self._global_stack.containersChanged.disconnect(self._triggerTimer)
@ -51,7 +55,7 @@ class AutoSave:
self._global_stack.propertyChanged.connect(self._triggerTimer) self._global_stack.propertyChanged.connect(self._triggerTimer)
self._global_stack.containersChanged.connect(self._triggerTimer) self._global_stack.containersChanged.connect(self._triggerTimer)
def _onTimeout(self): def _onTimeout(self) -> None:
self._saving = True # To prevent the save process from triggering another autosave. self._saving = True # To prevent the save process from triggering another autosave.
Logger.log("d", "Autosaving preferences, instances and profiles") Logger.log("d", "Autosaving preferences, instances and profiles")

View File

@ -145,6 +145,14 @@ class Backup:
# \return Whether we had success or not. # \return Whether we had success or not.
@staticmethod @staticmethod
def _extractArchive(archive: "ZipFile", target_path: str) -> bool: def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
# Implement security recommendations: Sanity check on zip files will make it harder to spoof.
from cura.CuraApplication import CuraApplication
config_filename = CuraApplication.getInstance().getApplicationName() + ".cfg" # Should be there if valid.
if config_filename not in [file.filename for file in archive.filelist]:
Logger.logException("e", "Unable to extract the backup due to corruption of compressed file(s).")
return False
Logger.log("d", "Removing current data in location: %s", target_path) Logger.log("d", "Removing current data in location: %s", target_path)
Resources.factoryReset() Resources.factoryReset()
Logger.log("d", "Extracting backup to location: %s", target_path) Logger.log("d", "Extracting backup to location: %s", target_path)

View File

@ -10,18 +10,23 @@ if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
## The BackupsManager is responsible for managing the creating and restoring of
# back-ups.
#
# Back-ups themselves are represented in a different class.
class BackupsManager: class BackupsManager:
"""
The BackupsManager is responsible for managing the creating and restoring of
back-ups.
Back-ups themselves are represented in a different class.
"""
def __init__(self, application: "CuraApplication") -> None: def __init__(self, application: "CuraApplication") -> None:
self._application = application self._application = application
## Get a back-up of the current configuration.
# \return A tuple containing a ZipFile (the actual back-up) and a dict
# containing some metadata (like version).
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]: def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
"""
Get a back-up of the current configuration.
:return: A tuple containing a ZipFile (the actual back-up) and a dict containing some metadata (like version).
"""
self._disableAutoSave() self._disableAutoSave()
backup = Backup(self._application) backup = Backup(self._application)
backup.makeFromCurrent() backup.makeFromCurrent()
@ -29,11 +34,13 @@ class BackupsManager:
# We don't return a Backup here because we want plugins only to interact with our API and not full objects. # We don't return a Backup here because we want plugins only to interact with our API and not full objects.
return backup.zip_file, backup.meta_data return backup.zip_file, backup.meta_data
## Restore a back-up from a given ZipFile.
# \param zip_file A bytes object containing the actual back-up.
# \param meta_data A dict containing some metadata that is needed to
# restore the back-up correctly.
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str]) -> None: def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str]) -> None:
"""
Restore a back-up from a given ZipFile.
:param zip_file: A bytes object containing the actual back-up.
:param meta_data: A dict containing some metadata that is needed to restore the back-up correctly.
"""
if not meta_data.get("cura_release", None): if not meta_data.get("cura_release", None):
# If there is no "cura_release" specified in the meta data, we don't execute a backup restore. # If there is no "cura_release" specified in the meta data, we don't execute a backup restore.
Logger.log("w", "Tried to restore a backup without specifying a Cura version number.") Logger.log("w", "Tried to restore a backup without specifying a Cura version number.")
@ -48,9 +55,10 @@ class BackupsManager:
# We don't want to store the data at this point as that would override the just-restored backup. # We don't want to store the data at this point as that would override the just-restored backup.
self._application.windowClosed(save_data = False) self._application.windowClosed(save_data = False)
## Here we try to disable the auto-save plug-in as it might interfere with
# restoring a back-up.
def _disableAutoSave(self) -> None: def _disableAutoSave(self) -> None:
"""Here we (try to) disable the saving as it might interfere with restoring a back-up."""
self._application.enableSave(False)
auto_save = self._application.getAutoSave() auto_save = self._application.getAutoSave()
# The auto save is only not created if the application has not yet started. # The auto save is only not created if the application has not yet started.
if auto_save: if auto_save:
@ -58,8 +66,10 @@ class BackupsManager:
else: else:
Logger.log("e", "Unable to disable the autosave as application init has not been completed") Logger.log("e", "Unable to disable the autosave as application init has not been completed")
## Re-enable auto-save after we're done.
def _enableAutoSave(self) -> None: def _enableAutoSave(self) -> None:
"""Re-enable auto-save and other saving after we're done."""
self._application.enableSave(True)
auto_save = self._application.getAutoSave() auto_save = self._application.getAutoSave()
# The auto save is only not created if the application has not yet started. # The auto save is only not created if the application has not yet started.
if auto_save: if auto_save:

View File

@ -1,15 +1,21 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import numpy
import math
from typing import List, Optional, TYPE_CHECKING, Any, Set, cast, Iterable, Dict
from UM.Mesh.MeshData import MeshData from UM.Mesh.MeshData import MeshData
from cura.Scene.CuraSceneNode import CuraSceneNode from UM.Mesh.MeshBuilder import MeshBuilder
from cura.Settings.ExtruderManager import ExtruderManager
from UM.Application import Application #To modify the maximum zoom level. from UM.Application import Application #To modify the maximum zoom level.
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Scene.Platform import Platform from UM.Scene.Platform import Platform
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Resources import Resources from UM.Resources import Resources
from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Math.Matrix import Matrix from UM.Math.Matrix import Matrix
from UM.Math.Color import Color from UM.Math.Color import Color
@ -17,23 +23,23 @@ from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Polygon import Polygon from UM.Math.Polygon import Polygon
from UM.Message import Message from UM.Message import Message
from UM.Signal import Signal from UM.Signal import Signal
from PyQt5.QtCore import QTimer
from UM.View.RenderBatch import RenderBatch from UM.View.RenderBatch import RenderBatch
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager
catalog = i18nCatalog("cura") from PyQt5.QtCore import QTimer
import numpy
import math
from typing import List, Optional, TYPE_CHECKING, Any, Set, cast, Iterable, Dict
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
catalog = i18nCatalog("cura")
# Radius of disallowed area in mm around prime. I.e. how much distance to keep from prime position. # Radius of disallowed area in mm around prime. I.e. how much distance to keep from prime position.
PRIME_CLEARANCE = 6.5 PRIME_CLEARANCE = 6.5
@ -1012,13 +1018,13 @@ class BuildVolume(SceneNode):
all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value") all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value")
all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type") all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type")
for i, (setting_value, setting_type) in enumerate(zip(all_values, all_types)): for i, (setting_value, setting_type) in enumerate(zip(all_values, all_types)):
if not setting_value and (setting_type == "int" or setting_type == "float"): if not setting_value and setting_type in ["int", "float"]:
all_values[i] = 0 all_values[i] = 0
return all_values return all_values
def _calculateBedAdhesionSize(self, used_extruders): def _calculateBedAdhesionSize(self, used_extruders):
if self._global_container_stack is None: if self._global_container_stack is None:
return return None
container_stack = self._global_container_stack container_stack = self._global_container_stack
adhesion_type = container_stack.getProperty("adhesion_type", "value") adhesion_type = container_stack.getProperty("adhesion_type", "value")

View File

@ -10,7 +10,7 @@ import os.path
import uuid import uuid
import json import json
import locale import locale
from typing import cast from typing import cast, Any
try: try:
from sentry_sdk.hub import Hub from sentry_sdk.hub import Hub
@ -32,6 +32,8 @@ from UM.Resources import Resources
from cura import ApplicationMetadata from cura import ApplicationMetadata
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
home_dir = os.path.expanduser("~")
MYPY = False MYPY = False
if MYPY: if MYPY:
@ -58,6 +60,8 @@ class CrashHandler:
self.traceback = tb self.traceback = tb
self.has_started = has_started self.has_started = has_started
self.dialog = None # Don't create a QDialog before there is a QApplication self.dialog = None # Don't create a QDialog before there is a QApplication
self.cura_version = None
self.cura_locale = None
Logger.log("c", "An uncaught error has occurred!") Logger.log("c", "An uncaught error has occurred!")
for line in traceback.format_exception(exception_type, value, tb): for line in traceback.format_exception(exception_type, value, tb):
@ -81,6 +85,21 @@ class CrashHandler:
self.dialog = QDialog() self.dialog = QDialog()
self._createDialog() self._createDialog()
@staticmethod
def pruneSensitiveData(obj: Any) -> Any:
if isinstance(obj, str):
return obj.replace(home_dir, "<user_home>")
if isinstance(obj, list):
return [CrashHandler.pruneSensitiveData(item) for item in obj]
if isinstance(obj, dict):
return {k: CrashHandler.pruneSensitiveData(v) for k, v in obj.items()}
return obj
@staticmethod
def sentryBeforeSend(event, hint):
return CrashHandler.pruneSensitiveData(event)
def _createEarlyCrashDialog(self): def _createEarlyCrashDialog(self):
dialog = QDialog() dialog = QDialog()
dialog.setMinimumWidth(500) dialog.setMinimumWidth(500)
@ -159,7 +178,6 @@ class CrashHandler:
layout.addWidget(self._informationWidget()) layout.addWidget(self._informationWidget())
layout.addWidget(self._exceptionInfoWidget()) layout.addWidget(self._exceptionInfoWidget())
layout.addWidget(self._logInfoWidget()) layout.addWidget(self._logInfoWidget())
layout.addWidget(self._userDescriptionWidget())
layout.addWidget(self._buttonsWidget()) layout.addWidget(self._buttonsWidget())
def _close(self): def _close(self):
@ -372,21 +390,6 @@ class CrashHandler:
return group return group
def _userDescriptionWidget(self):
group = QGroupBox()
group.setTitle(catalog.i18nc("@title:groupbox", "User description" +
" (Note: Developers may not speak your language, please use English if possible)"))
layout = QVBoxLayout()
# When sending the report, the user comments will be collected
self.user_description_text_area = QTextEdit()
self.user_description_text_area.setFocus(True)
layout.addWidget(self.user_description_text_area)
group.setLayout(layout)
return group
def _buttonsWidget(self): def _buttonsWidget(self):
buttons = QDialogButtonBox() buttons = QDialogButtonBox()
buttons.addButton(QDialogButtonBox.Close) buttons.addButton(QDialogButtonBox.Close)
@ -401,9 +404,6 @@ class CrashHandler:
return buttons return buttons
def _sendCrashReport(self): def _sendCrashReport(self):
# Before sending data, the user comments are stored
self.data["user_info"] = self.user_description_text_area.toPlainText()
if with_sentry_sdk: if with_sentry_sdk:
try: try:
hub = Hub.current hub = Hub.current

View File

@ -3,17 +3,15 @@
from PyQt5.QtCore import QObject, QUrl from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from typing import List, Optional, cast from typing import List, cast
from UM.Event import CallFunctionEvent from UM.Event import CallFunctionEvent
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Math.Quaternion import Quaternion
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.RotateOperation import RotateOperation
from UM.Operations.TranslateOperation import TranslateOperation from UM.Operations.TranslateOperation import TranslateOperation
import cura.CuraApplication import cura.CuraApplication

View File

@ -1,77 +1,59 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
import re
import sys import sys
import time import time
from typing import cast, TYPE_CHECKING, Optional, Callable, List from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any, Dict
import numpy import numpy
from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
from PyQt5.QtGui import QColor, QIcon from PyQt5.QtGui import QColor, QIcon
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
from PyQt5.QtWidgets import QMessageBox
from UM.i18n import i18nCatalog import UM.Util
import cura.Settings.cura_empty_instance_containers
from UM.Application import Application from UM.Application import Application
from UM.Decorators import override, deprecated from UM.Decorators import override
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message
from UM.Platform import Platform
from UM.PluginError import PluginNotFoundError
from UM.Resources import Resources
from UM.Preferences import Preferences
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
import UM.Util
from UM.View.SelectionPass import SelectionPass # For typing.
from UM.Math.AxisAlignedBox import AxisAlignedBox from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Matrix import Matrix from UM.Math.Matrix import Matrix
from UM.Math.Quaternion import Quaternion from UM.Math.Quaternion import Quaternion
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from UM.Mesh.ReadMeshJob import ReadMeshJob from UM.Mesh.ReadMeshJob import ReadMeshJob
from UM.Message import Message
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.SetTransformOperation import SetTransformOperation from UM.Operations.SetTransformOperation import SetTransformOperation
from UM.Platform import Platform
from UM.PluginError import PluginNotFoundError
from UM.Preferences import Preferences
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
from UM.Resources import Resources
from UM.Scene.Camera import Camera from UM.Scene.Camera import Camera
from UM.Scene.GroupDecorator import GroupDecorator from UM.Scene.GroupDecorator import GroupDecorator
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Scene.ToolHandle import ToolHandle from UM.Scene.ToolHandle import ToolHandle
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.Validator import Validator from UM.Settings.Validator import Validator
from UM.View.SelectionPass import SelectionPass # For typing.
from UM.Workspace.WorkspaceReader import WorkspaceReader from UM.Workspace.WorkspaceReader import WorkspaceReader
from UM.i18n import i18nCatalog
from cura import ApplicationMetadata
from cura.API import CuraAPI from cura.API import CuraAPI
from cura.Arranging.Arrange import Arrange from cura.Arranging.Arrange import Arrange
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.ShapeArray import ShapeArray from cura.Arranging.ShapeArray import ShapeArray
from cura.Operations.SetParentOperation import SetParentOperation
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from cura.Scene.CuraSceneController import CuraSceneController
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene import ZOffsetDecorator
from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Machines.MachineErrorChecker import MachineErrorChecker
from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel
@ -80,6 +62,8 @@ from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel
from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel
from cura.Machines.Models.IntentModel import IntentModel
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
@ -89,51 +73,47 @@ from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfile
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
from cura.Machines.Models.UserChangesModel import UserChangesModel from cura.Machines.Models.UserChangesModel import UserChangesModel
from cura.Machines.Models.IntentModel import IntentModel from cura.Operations.SetParentOperation import SetParentOperation
from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
import cura.Settings.cura_empty_instance_containers from cura.Scene import ZOffsetDecorator
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from cura.Scene.CuraSceneController import CuraSceneController
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Settings.ContainerManager import ContainerManager from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.IntentManager import IntentManager
from cura.Settings.MachineManager import MachineManager from cura.Settings.MachineManager import MachineManager
from cura.Settings.MachineNameValidator import MachineNameValidator from cura.Settings.MachineNameValidator import MachineNameValidator
from cura.Settings.IntentManager import IntentManager
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
from cura.UI.MachineSettingsManager import MachineSettingsManager from cura.UI.MachineSettingsManager import MachineSettingsManager
from cura.UI.ObjectsModel import ObjectsModel from cura.UI.ObjectsModel import ObjectsModel
from cura.UI.TextManager import TextManager
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
from cura.UI.RecommendedMode import RecommendedMode from cura.UI.RecommendedMode import RecommendedMode
from cura.UI.TextManager import TextManager
from cura.UI.WelcomePagesModel import WelcomePagesModel from cura.UI.WelcomePagesModel import WelcomePagesModel
from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel
from cura.UltimakerCloud import UltimakerCloudAuthentication
from cura.Utils.NetworkingUtil import NetworkingUtil from cura.Utils.NetworkingUtil import NetworkingUtil
from .SingleInstance import SingleInstance
from .AutoSave import AutoSave
from . import PlatformPhysics
from . import BuildVolume from . import BuildVolume
from . import CameraAnimation from . import CameraAnimation
from . import CuraActions from . import CuraActions
from . import PlatformPhysics
from . import PrintJobPreviewImageProvider from . import PrintJobPreviewImageProvider
from .AutoSave import AutoSave
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager from .SingleInstance import SingleInstance
from cura import ApplicationMetadata, UltimakerCloudAuthentication
from cura.Settings.GlobalStack import GlobalStack
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
@ -145,7 +125,7 @@ class CuraApplication(QtApplication):
# SettingVersion represents the set of settings available in the machine/extruder definitions. # SettingVersion represents the set of settings available in the machine/extruder definitions.
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible # You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
# changes of the settings. # changes of the settings.
SettingVersion = 11 SettingVersion = 13
Created = False Created = False
@ -191,9 +171,7 @@ class CuraApplication(QtApplication):
self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions] self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions]
self._cura_package_manager = None self._machine_action_manager = None # type: Optional[MachineActionManager.MachineActionManager]
self._machine_action_manager = None
self.empty_container = None # type: EmptyInstanceContainer self.empty_container = None # type: EmptyInstanceContainer
self.empty_definition_changes_container = None # type: EmptyInstanceContainer self.empty_definition_changes_container = None # type: EmptyInstanceContainer
@ -265,8 +243,8 @@ class CuraApplication(QtApplication):
# Backups # Backups
self._auto_save = None # type: Optional[AutoSave] self._auto_save = None # type: Optional[AutoSave]
self._enable_save = True
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
self._container_registry_class = CuraContainerRegistry self._container_registry_class = CuraContainerRegistry
# Redefined here in order to please the typing. # Redefined here in order to please the typing.
self._container_registry = None # type: CuraContainerRegistry self._container_registry = None # type: CuraContainerRegistry
@ -351,6 +329,9 @@ class CuraApplication(QtApplication):
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]: for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]:
Resources.addExpectedDirNameInData(dir_name) Resources.addExpectedDirNameInData(dir_name)
app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable)))
Resources.addSearchPath(os.path.join(app_root, "share", "cura", "resources"))
Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources")) Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
if not hasattr(sys, "frozen"): if not hasattr(sys, "frozen"):
resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources") resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
@ -394,6 +375,8 @@ class CuraApplication(QtApplication):
SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders) SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue) SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition) SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition)
SettingFunction.registerOperator("valueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndex)
SettingFunction.registerOperator("extruderValueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndexInExtruder)
# Adds all resources and container related resources. # Adds all resources and container related resources.
def __addAllResourcesAndContainerResources(self) -> None: def __addAllResourcesAndContainerResources(self) -> None:
@ -633,6 +616,12 @@ class CuraApplication(QtApplication):
def showPreferences(self) -> None: def showPreferences(self) -> None:
self.showPreferencesWindow.emit() self.showPreferencesWindow.emit()
# This is called by drag-and-dropping curapackage files.
@pyqtSlot(QUrl)
def installPackageViaDragAndDrop(self, file_url: str) -> Optional[str]:
filename = QUrl(file_url).toLocalFile()
return self._package_manager.installPackage(filename)
@override(Application) @override(Application)
def getGlobalContainerStack(self) -> Optional["GlobalStack"]: def getGlobalContainerStack(self) -> Optional["GlobalStack"]:
return self._global_container_stack return self._global_container_stack
@ -698,15 +687,20 @@ class CuraApplication(QtApplication):
self._message_box_callback = None self._message_box_callback = None
self._message_box_callback_arguments = [] self._message_box_callback_arguments = []
def enableSave(self, enable: bool):
self._enable_save = enable
# Cura has multiple locations where instance containers need to be saved, so we need to handle this differently. # Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
def saveSettings(self): def saveSettings(self) -> None:
if not self.started: if not self.started or not self._enable_save:
# Do not do saving during application start or when data should not be saved on quit. # Do not do saving during application start or when data should not be saved on quit.
return return
ContainerRegistry.getInstance().saveDirtyContainers() ContainerRegistry.getInstance().saveDirtyContainers()
self.savePreferences() self.savePreferences()
def saveStack(self, stack): def saveStack(self, stack):
if not self._enable_save:
return
ContainerRegistry.getInstance().saveContainer(stack) ContainerRegistry.getInstance().saveContainer(stack)
@pyqtSlot(str, result = QUrl) @pyqtSlot(str, result = QUrl)
@ -989,8 +983,8 @@ class CuraApplication(QtApplication):
## Get the machine action manager ## Get the machine action manager
# We ignore any *args given to this, as we also register the machine manager as qml singleton. # We ignore any *args given to this, as we also register the machine manager as qml singleton.
# It wants to give this function an engine and script engine, but we don't care about that. # It wants to give this function an engine and script engine, but we don't care about that.
def getMachineActionManager(self, *args): def getMachineActionManager(self, *args: Any) -> MachineActionManager.MachineActionManager:
return self._machine_action_manager return cast(MachineActionManager.MachineActionManager, self._machine_action_manager)
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getMaterialManagementModel(self) -> MaterialManagementModel: def getMaterialManagementModel(self) -> MaterialManagementModel:
@ -1389,12 +1383,21 @@ class CuraApplication(QtApplication):
if not nodes: if not nodes:
return return
objects_in_filename = {} # type: Dict[str, List[CuraSceneNode]]
for node in nodes: for node in nodes:
mesh_data = node.getMeshData() mesh_data = node.getMeshData()
if mesh_data: if mesh_data:
file_name = mesh_data.getFileName() file_name = mesh_data.getFileName()
if file_name: if file_name:
if file_name not in objects_in_filename:
objects_in_filename[file_name] = []
if file_name in objects_in_filename:
objects_in_filename[file_name].append(node)
else:
Logger.log("w", "Unable to reload data because we don't have a filename.")
for file_name, nodes in objects_in_filename.items():
for node in nodes:
job = ReadMeshJob(file_name) job = ReadMeshJob(file_name)
job._node = node # type: ignore job._node = node # type: ignore
job.finished.connect(self._reloadMeshFinished) job.finished.connect(self._reloadMeshFinished)
@ -1402,8 +1405,6 @@ class CuraApplication(QtApplication):
job.finished.connect(self.updateOriginOfMergedMeshes) job.finished.connect(self.updateOriginOfMergedMeshes)
job.start() job.start()
else:
Logger.log("w", "Unable to reload data because we don't have a filename.")
@pyqtSlot("QStringList") @pyqtSlot("QStringList")
def setExpandedCategories(self, categories: List[str]) -> None: def setExpandedCategories(self, categories: List[str]) -> None:
@ -1443,7 +1444,7 @@ class CuraApplication(QtApplication):
if center is not None: if center is not None:
object_centers.append(center) object_centers.append(center)
if object_centers and len(object_centers) > 0: if object_centers:
middle_x = sum([v.x for v in object_centers]) / len(object_centers) middle_x = sum([v.x for v in object_centers]) / len(object_centers)
middle_y = sum([v.y for v in object_centers]) / len(object_centers) middle_y = sum([v.y for v in object_centers]) / len(object_centers)
middle_z = sum([v.z for v in object_centers]) / len(object_centers) middle_z = sum([v.z for v in object_centers]) / len(object_centers)
@ -1493,7 +1494,7 @@ class CuraApplication(QtApplication):
if center is not None: if center is not None:
object_centers.append(center) object_centers.append(center)
if object_centers and len(object_centers) > 0: if object_centers:
middle_x = sum([v.x for v in object_centers]) / len(object_centers) middle_x = sum([v.x for v in object_centers]) / len(object_centers)
middle_y = sum([v.y for v in object_centers]) / len(object_centers) middle_y = sum([v.y for v in object_centers]) / len(object_centers)
middle_z = sum([v.z for v in object_centers]) / len(object_centers) middle_z = sum([v.z for v in object_centers]) / len(object_centers)
@ -1579,13 +1580,30 @@ class CuraApplication(QtApplication):
fileLoaded = pyqtSignal(str) fileLoaded = pyqtSignal(str)
fileCompleted = pyqtSignal(str) fileCompleted = pyqtSignal(str)
def _reloadMeshFinished(self, job): def _reloadMeshFinished(self, job) -> None:
# TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh! """
job_result = job.getResult() Function called whenever a ReadMeshJob finishes in the background. It reloads a specific node object in the
scene from its source file. The function gets all the nodes that exist in the file through the job result, and
then finds the scene node that it wants to refresh by its object id. Each job refreshes only one node.
:param job: The ReadMeshJob running in the background that reads all the meshes in a file
:return: None
"""
job_result = job.getResult() # nodes that exist inside the file read by this job
if len(job_result) == 0: if len(job_result) == 0:
Logger.log("e", "Reloading the mesh failed.") Logger.log("e", "Reloading the mesh failed.")
return return
mesh_data = job_result[0].getMeshData() object_found = False
mesh_data = None
# Find the node to be refreshed based on its id
for job_result_node in job_result:
if job_result_node.getId() == job._node.getId():
mesh_data = job_result_node.getMeshData()
object_found = True
break
if not object_found:
Logger.warning("The object with id {} no longer exists! Keeping the old version in the scene.".format(job_result_node.getId()))
return
if not mesh_data: if not mesh_data:
Logger.log("w", "Could not find a mesh in reloaded node.") Logger.log("w", "Could not find a mesh in reloaded node.")
return return
@ -1675,7 +1693,7 @@ class CuraApplication(QtApplication):
extension = os.path.splitext(f)[1] extension = os.path.splitext(f)[1]
extension = extension.lower() extension = extension.lower()
filename = os.path.basename(f) filename = os.path.basename(f)
if len(self._currently_loading_files) > 0: if self._currently_loading_files:
# If a non-slicable file is already being loaded, we prevent loading of any further non-slicable files # If a non-slicable file is already being loaded, we prevent loading of any further non-slicable files
if extension in self._non_sliceable_extensions: if extension in self._non_sliceable_extensions:
message = Message( message = Message(
@ -1796,8 +1814,8 @@ class CuraApplication(QtApplication):
node.addDecorator(build_plate_decorator) node.addDecorator(build_plate_decorator)
build_plate_decorator.setBuildPlateNumber(target_build_plate) build_plate_decorator.setBuildPlateNumber(target_build_plate)
op = AddSceneNodeOperation(node, scene.getRoot()) operation = AddSceneNodeOperation(node, scene.getRoot())
op.push() operation.push()
node.callDecoration("setActiveExtruder", default_extruder_id) node.callDecoration("setActiveExtruder", default_extruder_id)
scene.sceneChanged.emit(node) scene.sceneChanged.emit(node)
@ -1828,11 +1846,17 @@ class CuraApplication(QtApplication):
def _onContextMenuRequested(self, x: float, y: float) -> None: def _onContextMenuRequested(self, x: float, y: float) -> None:
# Ensure we select the object if we request a context menu over an object without having a selection. # Ensure we select the object if we request a context menu over an object without having a selection.
if not Selection.hasSelection(): if Selection.hasSelection():
node = self.getController().getScene().findObject(cast(SelectionPass, self.getRenderer().getRenderPass("selection")).getIdAtPosition(x, y)) return
if node: selection_pass = cast(SelectionPass, self.getRenderer().getRenderPass("selection"))
if not selection_pass: # If you right-click before the rendering has been initialised there might not be a selection pass yet.
print("--------------ding! Got the crash.")
return
node = self.getController().getScene().findObject(selection_pass.getIdAtPosition(x, y))
if not node:
return
parent = node.getParent() parent = node.getParent()
while(parent and parent.callDecoration("isGroup")): while parent and parent.callDecoration("isGroup"):
node = parent node = parent
parent = node.getParent() parent = node.getParent()
@ -1871,7 +1895,6 @@ class CuraApplication(QtApplication):
main_window = QtApplication.getInstance().getMainWindow() main_window = QtApplication.getInstance().getMainWindow()
if main_window: if main_window:
return main_window.width() return main_window.width()
else:
return 0 return 0
@pyqtSlot(result = int) @pyqtSlot(result = int)
@ -1879,7 +1902,6 @@ class CuraApplication(QtApplication):
main_window = QtApplication.getInstance().getMainWindow() main_window = QtApplication.getInstance().getMainWindow()
if main_window: if main_window:
return main_window.height() return main_window.height()
else:
return 0 return 0
@pyqtSlot() @pyqtSlot()

View File

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import List, Tuple from typing import List, Tuple, TYPE_CHECKING, Optional
from cura.CuraApplication import CuraApplication #To find some resource types. from cura.CuraApplication import CuraApplication #To find some resource types.
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
@ -9,12 +9,16 @@ from cura.Settings.GlobalStack import GlobalStack
from UM.PackageManager import PackageManager #The class we're extending. from UM.PackageManager import PackageManager #The class we're extending.
from UM.Resources import Resources #To find storage paths for some resource types. from UM.Resources import Resources #To find storage paths for some resource types.
if TYPE_CHECKING:
from UM.Qt.QtApplication import QtApplication
from PyQt5.QtCore import QObject
class CuraPackageManager(PackageManager): class CuraPackageManager(PackageManager):
def __init__(self, application, parent = None): def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None:
super().__init__(application, parent) super().__init__(application, parent)
def initialize(self): def initialize(self) -> None:
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer) self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer) self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
CuraAppName = "@CURA_APP_NAME@" CuraAppName = "@CURA_APP_NAME@"
@ -9,3 +9,4 @@ CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@" CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@" CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@" CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
CuraMarketplaceRoot = "@CURA_MARKETPLACE_ROOT@"

View File

@ -26,6 +26,7 @@ class CuraView(View):
def mainComponent(self) -> QUrl: def mainComponent(self) -> QUrl:
return self.getDisplayComponent("main") return self.getDisplayComponent("main")
@pyqtProperty(QUrl, constant = True) @pyqtProperty(QUrl, constant = True)
def stageMenuComponent(self) -> QUrl: def stageMenuComponent(self) -> QUrl:
url = self.getDisplayComponent("menu") url = self.getDisplayComponent("menu")

View File

@ -33,10 +33,10 @@ class Layer:
def elementCount(self): def elementCount(self):
return self._element_count return self._element_count
def setHeight(self, height): def setHeight(self, height: float) -> None:
self._height = height self._height = height
def setThickness(self, thickness): def setThickness(self, thickness: float) -> None:
self._thickness = thickness self._thickness = thickness
def lineMeshVertexCount(self) -> int: def lineMeshVertexCount(self) -> int:

View File

@ -16,7 +16,6 @@ class LayerData(MeshData):
def getLayer(self, layer): def getLayer(self, layer):
if layer in self._layers: if layer in self._layers:
return self._layers[layer] return self._layers[layer]
else:
return None return None
def getLayers(self): def getLayers(self):

View File

@ -9,7 +9,7 @@ from cura.LayerData import LayerData
## Simple decorator to indicate a scene node holds layer data. ## Simple decorator to indicate a scene node holds layer data.
class LayerDataDecorator(SceneNodeDecorator): class LayerDataDecorator(SceneNodeDecorator):
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self._layer_data = None # type: Optional[LayerData] self._layer_data = None # type: Optional[LayerData]

View File

@ -1,10 +1,11 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Qt.QtApplication import QtApplication
from typing import Any, Optional
import numpy import numpy
from typing import Optional, cast
from UM.Qt.Bindings.Theme import Theme
from UM.Qt.QtApplication import QtApplication
from UM.Logger import Logger from UM.Logger import Logger
@ -61,7 +62,7 @@ class LayerPolygon:
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType # When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
# Should be generated in better way, not hardcoded. # Should be generated in better way, not hardcoded.
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = numpy.bool) self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = numpy.bool)
self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray] self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
self._build_cache_needed_points = None # type: Optional[numpy.ndarray] self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
@ -149,17 +150,17 @@ class LayerPolygon:
def getColors(self): def getColors(self):
return self._colors return self._colors
def mapLineTypeToColor(self, line_types): def mapLineTypeToColor(self, line_types: numpy.ndarray) -> numpy.ndarray:
return self._color_map[line_types] return self._color_map[line_types]
def isInfillOrSkinType(self, line_types): def isInfillOrSkinType(self, line_types: numpy.ndarray) -> numpy.ndarray:
return self._isInfillOrSkinTypeMap[line_types] return self._is_infill_or_skin_type_map[line_types]
def lineMeshVertexCount(self): def lineMeshVertexCount(self) -> int:
return (self._vertex_end - self._vertex_begin) return self._vertex_end - self._vertex_begin
def lineMeshElementCount(self): def lineMeshElementCount(self) -> int:
return (self._index_end - self._index_begin) return self._index_end - self._index_begin
@property @property
def extruder(self): def extruder(self):
@ -202,7 +203,7 @@ class LayerPolygon:
return self._jump_count return self._jump_count
# Calculate normals for the entire polygon using numpy. # Calculate normals for the entire polygon using numpy.
def getNormals(self): def getNormals(self) -> numpy.ndarray:
normals = numpy.copy(self._data) normals = numpy.copy(self._data)
normals[:, 1] = 0.0 # We are only interested in 2D normals normals[:, 1] = 0.0 # We are only interested in 2D normals
@ -226,13 +227,13 @@ class LayerPolygon:
return normals return normals
__color_map = None # type: numpy.ndarray[Any] __color_map = None # type: numpy.ndarray
## Gets the instance of the VersionUpgradeManager, or creates one. ## Gets the instance of the VersionUpgradeManager, or creates one.
@classmethod @classmethod
def getColorMap(cls): def getColorMap(cls) -> numpy.ndarray:
if cls.__color_map is None: if cls.__color_map is None:
theme = QtApplication.getInstance().getTheme() theme = cast(Theme, QtApplication.getInstance().getTheme())
cls.__color_map = numpy.array([ cls.__color_map = numpy.array([
theme.getColor("layerview_none").getRgbF(), # NoneType theme.getColor("layerview_none").getRgbF(), # NoneType
theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type

View File

@ -26,7 +26,7 @@ class ContainerNode:
## Gets the metadata of the container that this node represents. ## Gets the metadata of the container that this node represents.
# Getting the metadata from the container directly is about 10x as fast. # Getting the metadata from the container directly is about 10x as fast.
# \return The metadata of the container in this node. # \return The metadata of the container in this node.
def getMetadata(self): def getMetadata(self) -> Dict[str, Any]:
return ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)[0] return ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)[0]
## Get an entry from the metadata of the container that this node contains. ## Get an entry from the metadata of the container that this node contains.

View File

@ -30,7 +30,7 @@ if TYPE_CHECKING:
# nodes that have children) but that child node may be a node representing the # nodes that have children) but that child node may be a node representing the
# empty instance container. # empty instance container.
class ContainerTree: class ContainerTree:
__instance = None __instance = None # type: Optional["ContainerTree"]
@classmethod @classmethod
def getInstance(cls): def getInstance(cls):
@ -75,7 +75,7 @@ class ContainerTree:
return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled) return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
## Ran after completely starting up the application. ## Ran after completely starting up the application.
def _onStartupFinished(self): def _onStartupFinished(self) -> None:
currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks. currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks.
JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added)) JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added))
@ -137,7 +137,7 @@ class ContainerTree:
# \param container_stacks All of the stacks to pre-load the container # \param container_stacks All of the stacks to pre-load the container
# trees for. This needs to be provided from here because the stacks # trees for. This needs to be provided from here because the stacks
# need to be constructed on the main thread because they are QObject. # need to be constructed on the main thread because they are QObject.
def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]): def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]) -> None:
self.tree_root = tree_root self.tree_root = tree_root
self.container_stacks = container_stacks self.container_stacks = container_stacks
super().__init__() super().__init__()

View File

@ -6,13 +6,13 @@ import time
from collections import deque from collections import deque
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty
from typing import Optional, Any, Set
from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Settings.SettingDefinition import SettingDefinition from UM.Settings.SettingDefinition import SettingDefinition
from UM.Settings.Validator import ValidatorState from UM.Settings.Validator import ValidatorState
import cura.CuraApplication
# #
# This class performs setting error checks for the currently active machine. # This class performs setting error checks for the currently active machine.
# #
@ -24,25 +24,25 @@ from UM.Settings.Validator import ValidatorState
# #
class MachineErrorChecker(QObject): class MachineErrorChecker(QObject):
def __init__(self, parent = None): def __init__(self, parent: Optional[QObject] = None) -> None:
super().__init__(parent) super().__init__(parent)
self._global_stack = None self._global_stack = None
self._has_errors = True # Result of the error check, indicating whether there are errors in the stack self._has_errors = True # Result of the error check, indicating whether there are errors in the stack
self._error_keys = set() # A set of settings keys that have errors self._error_keys = set() # type: Set[str] # A set of settings keys that have errors
self._error_keys_in_progress = set() # The variable that stores the results of the currently in progress check self._error_keys_in_progress = set() # type: Set[str] # The variable that stores the results of the currently in progress check
self._stacks_and_keys_to_check = None # a FIFO queue of tuples (stack, key) to check for errors self._stacks_and_keys_to_check = None # type: Optional[deque] # a FIFO queue of tuples (stack, key) to check for errors
self._need_to_check = False # Whether we need to schedule a new check or not. This flag is set when a new self._need_to_check = False # Whether we need to schedule a new check or not. This flag is set when a new
# error check needs to take place while there is already one running at the moment. # error check needs to take place while there is already one running at the moment.
self._check_in_progress = False # Whether there is an error check running in progress at the moment. self._check_in_progress = False # Whether there is an error check running in progress at the moment.
self._application = Application.getInstance() self._application = cura.CuraApplication.CuraApplication.getInstance()
self._machine_manager = self._application.getMachineManager() self._machine_manager = self._application.getMachineManager()
self._start_time = 0 # measure checking time self._start_time = 0. # measure checking time
# This timer delays the starting of error check so we can react less frequently if the user is frequently # This timer delays the starting of error check so we can react less frequently if the user is frequently
# changing settings. # changing settings.
@ -94,13 +94,13 @@ class MachineErrorChecker(QObject):
# Start the error check for property changed # Start the error check for property changed
# this is seperate from the startErrorCheck because it ignores a number property types # this is seperate from the startErrorCheck because it ignores a number property types
def startErrorCheckPropertyChanged(self, key, property_name): def startErrorCheckPropertyChanged(self, key: str, property_name: str) -> None:
if property_name != "value": if property_name != "value":
return return
self.startErrorCheck() self.startErrorCheck()
# Starts the error check timer to schedule a new error check. # Starts the error check timer to schedule a new error check.
def startErrorCheck(self, *args) -> None: def startErrorCheck(self, *args: Any) -> None:
if not self._check_in_progress: if not self._check_in_progress:
self._need_to_check = True self._need_to_check = True
self.needToWaitForResultChanged.emit() self.needToWaitForResultChanged.emit()

View File

@ -176,9 +176,9 @@ class MachineNode(ContainerNode):
# Find the global qualities for this printer. # Find the global qualities for this printer.
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.quality_definition, global_quality = "True") # First try specific to this printer. global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.quality_definition, global_quality = "True") # First try specific to this printer.
if len(global_qualities) == 0: # This printer doesn't override the global qualities. if not global_qualities: # This printer doesn't override the global qualities.
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter", global_quality = "True") # Otherwise pick the global global qualities. global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter", global_quality = "True") # Otherwise pick the global global qualities.
if len(global_qualities) == 0: # There are no global qualities either?! Something went very wrong, but we'll not crash and properly fill the tree. if not global_qualities: # There are no global qualities either?! Something went very wrong, but we'll not crash and properly fill the tree.
global_qualities = [cura.CuraApplication.CuraApplication.getInstance().empty_quality_container.getMetaData()] global_qualities = [cura.CuraApplication.CuraApplication.getInstance().empty_quality_container.getMetaData()]
for global_quality in global_qualities: for global_quality in global_qualities:
self.global_qualities[global_quality["quality_type"]] = QualityNode(global_quality["id"], parent = self) self.global_qualities[global_quality["quality_type"]] = QualityNode(global_quality["id"], parent = self)

View File

@ -14,6 +14,7 @@ if TYPE_CHECKING:
from typing import Dict from typing import Dict
from cura.Machines.VariantNode import VariantNode from cura.Machines.VariantNode import VariantNode
## Represents a material in the container tree. ## Represents a material in the container tree.
# #
# Its subcontainers are quality profiles. # Its subcontainers are quality profiles.

View File

@ -114,7 +114,10 @@ class IntentModel(ListModel):
Logger.log("w", "Could not find the variant %s", active_variant_name) Logger.log("w", "Could not find the variant %s", active_variant_name)
continue continue
active_variant_node = machine_node.variants[active_variant_name] active_variant_node = machine_node.variants[active_variant_name]
active_material_node = active_variant_node.materials[extruder.material.getMetaDataEntry("base_file")] active_material_node = active_variant_node.materials.get(extruder.material.getMetaDataEntry("base_file"))
if active_material_node is None:
Logger.log("w", "Could not find the material %s", extruder.material.getMetaDataEntry("base_file"))
continue
nodes.add(active_material_node) nodes.add(active_material_node)
return nodes return nodes

View File

@ -34,7 +34,7 @@ class MaterialBrandsModel(BaseMaterialsModel):
brand_item_list = [] brand_item_list = []
brand_group_dict = {} brand_group_dict = {}
# Part 1: Generate the entire tree of brands -> material types -> spcific materials # Part 1: Generate the entire tree of brands -> material types -> specific materials
for root_material_id, container_node in self._available_materials.items(): for root_material_id, container_node in self._available_materials.items():
# Do not include the materials from a to-be-removed package # Do not include the materials from a to-be-removed package
if bool(container_node.getMetaDataEntry("removed", False)): if bool(container_node.getMetaDataEntry("removed", False)):

View File

@ -2,6 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
from typing import Set
import cura.CuraApplication import cura.CuraApplication
from UM.Logger import Logger from UM.Logger import Logger
@ -23,7 +24,7 @@ class QualitySettingsModel(ListModel):
GLOBAL_STACK_POSITION = -1 GLOBAL_STACK_POSITION = -1
def __init__(self, parent = None): def __init__(self, parent = None) -> None:
super().__init__(parent = parent) super().__init__(parent = parent)
self.addRoleName(self.KeyRole, "key") self.addRoleName(self.KeyRole, "key")
@ -38,7 +39,9 @@ class QualitySettingsModel(ListModel):
self._application = cura.CuraApplication.CuraApplication.getInstance() self._application = cura.CuraApplication.CuraApplication.getInstance()
self._application.getMachineManager().activeStackChanged.connect(self._update) self._application.getMachineManager().activeStackChanged.connect(self._update)
self._selected_position = self.GLOBAL_STACK_POSITION #Must be either GLOBAL_STACK_POSITION or an extruder position (0, 1, etc.) # Must be either GLOBAL_STACK_POSITION or an extruder position (0, 1, etc.)
self._selected_position = self.GLOBAL_STACK_POSITION
self._selected_quality_item = None # The selected quality in the quality management page self._selected_quality_item = None # The selected quality in the quality management page
self._i18n_catalog = None self._i18n_catalog = None
@ -47,14 +50,14 @@ class QualitySettingsModel(ListModel):
selectedPositionChanged = pyqtSignal() selectedPositionChanged = pyqtSignal()
selectedQualityItemChanged = pyqtSignal() selectedQualityItemChanged = pyqtSignal()
def setSelectedPosition(self, selected_position): def setSelectedPosition(self, selected_position: int) -> None:
if selected_position != self._selected_position: if selected_position != self._selected_position:
self._selected_position = selected_position self._selected_position = selected_position
self.selectedPositionChanged.emit() self.selectedPositionChanged.emit()
self._update() self._update()
@pyqtProperty(int, fset = setSelectedPosition, notify = selectedPositionChanged) @pyqtProperty(int, fset = setSelectedPosition, notify = selectedPositionChanged)
def selectedPosition(self): def selectedPosition(self) -> int:
return self._selected_position return self._selected_position
def setSelectedQualityItem(self, selected_quality_item): def setSelectedQualityItem(self, selected_quality_item):
@ -67,7 +70,7 @@ class QualitySettingsModel(ListModel):
def selectedQualityItem(self): def selectedQualityItem(self):
return self._selected_quality_item return self._selected_quality_item
def _update(self): def _update(self) -> None:
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
if not self._selected_quality_item: if not self._selected_quality_item:
@ -83,7 +86,7 @@ class QualitySettingsModel(ListModel):
quality_changes_group = self._selected_quality_item["quality_changes_group"] quality_changes_group = self._selected_quality_item["quality_changes_group"]
quality_node = None quality_node = None
settings_keys = set() settings_keys = set() # type: Set[str]
if quality_group: if quality_group:
if self._selected_position == self.GLOBAL_STACK_POSITION: if self._selected_position == self.GLOBAL_STACK_POSITION:
quality_node = quality_group.node_for_global quality_node = quality_group.node_for_global

View File

@ -51,7 +51,7 @@ class VariantNode(ContainerNode):
# Find all the materials for this variant's name. # Find all the materials for this variant's name.
else: # Printer has its own material profiles. Look for material profiles with this printer's definition. else: # Printer has its own material profiles. Look for material profiles with this printer's definition.
base_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = "fdmprinter") base_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = "fdmprinter")
printer_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id, variant_name = None) printer_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id)
variant_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id, variant_name = self.variant_name) # If empty_variant, this won't return anything. variant_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id, variant_name = self.variant_name) # If empty_variant, this won't return anything.
materials_per_base_file = {material["base_file"]: material for material in base_materials} materials_per_base_file = {material["base_file"]: material for material in base_materials}
materials_per_base_file.update({material["base_file"]: material for material in printer_specific_materials}) # Printer-specific profiles override global ones. materials_per_base_file.update({material["base_file"]: material for material in printer_specific_materials}) # Printer-specific profiles override global ones.

View File

@ -47,7 +47,7 @@ class MultiplyObjectsJob(Job):
nodes = [] nodes = []
not_fit_count = 0 not_fit_count = 0
found_solution_for_all = False
for node in self._objects: for node in self._objects:
# If object is part of a group, multiply group # If object is part of a group, multiply group
current_node = node current_node = node
@ -66,7 +66,7 @@ class MultiplyObjectsJob(Job):
found_solution_for_all = True found_solution_for_all = True
arranger.resetLastPriority() arranger.resetLastPriority()
for i in range(self._count): for _ in range(self._count):
# We do place the nodes one by one, as we want to yield in between. # We do place the nodes one by one, as we want to yield in between.
new_node = copy.deepcopy(node) new_node = copy.deepcopy(node)
solution_found = False solution_found = False
@ -98,10 +98,10 @@ class MultiplyObjectsJob(Job):
Job.yieldThread() Job.yieldThread()
if nodes: if nodes:
op = GroupedOperation() operation = GroupedOperation()
for new_node in nodes: for new_node in nodes:
op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent())) operation.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
op.push() operation.push()
status_message.hide() status_message.hide()
if not found_solution_for_all: if not found_solution_for_all:

View File

@ -115,9 +115,10 @@ class AuthorizationHelpers:
) )
@staticmethod @staticmethod
## Generate a 16-character verification code. ## Generate a verification code of arbitrary length.
# \param code_length: How long should the code be? # \param code_length: How long should the code be? This should never be lower than 16, but it's probably better to
def generateVerificationCode(code_length: int = 16) -> str: # leave it at 32
def generateVerificationCode(code_length: int = 32) -> str:
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length)) return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
@staticmethod @staticmethod

View File

@ -25,6 +25,8 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]] self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
self.verification_code = None # type: Optional[str] self.verification_code = None # type: Optional[str]
self.state = None # type: Optional[str]
# CURA-6609: Some browser seems to issue a HEAD instead of GET request as the callback. # CURA-6609: Some browser seems to issue a HEAD instead of GET request as the callback.
def do_HEAD(self) -> None: def do_HEAD(self) -> None:
self.do_GET() self.do_GET()
@ -58,7 +60,14 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
# \return HTTP ResponseData containing a success page to show to the user. # \return HTTP ResponseData containing a success page to show to the user.
def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]: def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]:
code = self._queryGet(query, "code") code = self._queryGet(query, "code")
if code and self.authorization_helpers is not None and self.verification_code is not None: state = self._queryGet(query, "state")
if state != self.state:
token_response = AuthenticationResponse(
success = False,
err_message=catalog.i18nc("@message",
"The provided state is not correct.")
)
elif code and self.authorization_helpers is not None and self.verification_code is not None:
# If the code was returned we get the access token. # If the code was returned we get the access token.
token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode( token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode(
code, self.verification_code) code, self.verification_code)

View File

@ -25,3 +25,6 @@ class AuthorizationRequestServer(HTTPServer):
## Set the verification code on the request handler. ## Set the verification code on the request handler.
def setVerificationCode(self, verification_code: str) -> None: def setVerificationCode(self, verification_code: str) -> None:
self.RequestHandlerClass.verification_code = verification_code # type: ignore self.RequestHandlerClass.verification_code = verification_code # type: ignore
def setState(self, state: str) -> None:
self.RequestHandlerClass.state = state # type: ignore

View File

@ -153,13 +153,15 @@ class AuthorizationService:
verification_code = self._auth_helpers.generateVerificationCode() verification_code = self._auth_helpers.generateVerificationCode()
challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code) challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code)
state = AuthorizationHelpers.generateVerificationCode()
# Create the query string needed for the OAuth2 flow. # Create the query string needed for the OAuth2 flow.
query_string = urlencode({ query_string = urlencode({
"client_id": self._settings.CLIENT_ID, "client_id": self._settings.CLIENT_ID,
"redirect_uri": self._settings.CALLBACK_URL, "redirect_uri": self._settings.CALLBACK_URL,
"scope": self._settings.CLIENT_SCOPES, "scope": self._settings.CLIENT_SCOPES,
"response_type": "code", "response_type": "code",
"state": "(.Y.)", "state": state, # Forever in our Hearts, RIP "(.Y.)" (2018-2020)
"code_challenge": challenge_code, "code_challenge": challenge_code,
"code_challenge_method": "S512" "code_challenge_method": "S512"
}) })
@ -168,7 +170,7 @@ class AuthorizationService:
QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string))) QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string)))
# Start a local web server to receive the callback URL on. # Start a local web server to receive the callback URL on.
self._server.start(verification_code) self._server.start(verification_code, state)
## Callback method for the authentication flow. ## Callback method for the authentication flow.
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None: def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:

View File

@ -1,13 +1,18 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import threading import threading
from typing import Optional, Callable, Any, TYPE_CHECKING from typing import Any, Callable, Optional, TYPE_CHECKING
from UM.Logger import Logger from UM.Logger import Logger
got_server_type = False
try:
from cura.OAuth2.AuthorizationRequestServer import AuthorizationRequestServer from cura.OAuth2.AuthorizationRequestServer import AuthorizationRequestServer
from cura.OAuth2.AuthorizationRequestHandler import AuthorizationRequestHandler from cura.OAuth2.AuthorizationRequestHandler import AuthorizationRequestHandler
got_server_type = True
except PermissionError: # Bug in http.server: Can't access MIME types. This will prevent the user from logging in. See Sentry bug Cura-3Q.
Logger.error("Can't start a server due to a PermissionError when starting the http.server.")
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.OAuth2.Models import AuthenticationResponse from cura.OAuth2.Models import AuthenticationResponse
@ -36,7 +41,8 @@ class LocalAuthorizationServer:
## Starts the local web server to handle the authorization callback. ## Starts the local web server to handle the authorization callback.
# \param verification_code The verification code part of the OAuth2 client identification. # \param verification_code The verification code part of the OAuth2 client identification.
def start(self, verification_code: str) -> None: # \param state The unique state code (to ensure that the request we get back is really from the server.
def start(self, verification_code: str, state: str) -> None:
if self._web_server: if self._web_server:
# If the server is already running (because of a previously aborted auth flow), we don't have to start it. # If the server is already running (because of a previously aborted auth flow), we don't have to start it.
# We still inject the new verification code though. # We still inject the new verification code though.
@ -49,10 +55,12 @@ class LocalAuthorizationServer:
Logger.log("d", "Starting local web server to handle authorization callback on port %s", self._web_server_port) Logger.log("d", "Starting local web server to handle authorization callback on port %s", self._web_server_port)
# Create the server and inject the callback and code. # Create the server and inject the callback and code.
if got_server_type:
self._web_server = AuthorizationRequestServer(("0.0.0.0", self._web_server_port), AuthorizationRequestHandler) self._web_server = AuthorizationRequestServer(("0.0.0.0", self._web_server_port), AuthorizationRequestHandler)
self._web_server.setAuthorizationHelpers(self._auth_helpers) self._web_server.setAuthorizationHelpers(self._auth_helpers)
self._web_server.setAuthorizationCallback(self._auth_state_changed_callback) self._web_server.setAuthorizationCallback(self._auth_state_changed_callback)
self._web_server.setVerificationCode(verification_code) self._web_server.setVerificationCode(verification_code)
self._web_server.setState(state)
# Start the server on a new thread. # Start the server on a new thread.
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon) self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)

View File

@ -1,10 +1,10 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional from typing import Optional, Dict, Any
class BaseModel: class BaseModel:
def __init__(self, **kwargs): def __init__(self, **kwargs: Any) -> None:
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
@ -53,9 +53,10 @@ class ResponseData(BaseModel):
redirect_uri = None # type: Optional[str] redirect_uri = None # type: Optional[str]
content_type = "text/html" # type: str content_type = "text/html" # type: str
## Possible HTTP responses. ## Possible HTTP responses.
HTTP_STATUS = { HTTP_STATUS = {
"OK": ResponseStatus(code = 200, message = "OK"), "OK": ResponseStatus(code = 200, message = "OK"),
"NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"), "NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"),
"REDIRECT": ResponseStatus(code = 302, message = "REDIRECT") "REDIRECT": ResponseStatus(code = 302, message = "REDIRECT")
} } # type: Dict[str, ResponseStatus]

View File

@ -122,6 +122,6 @@ class _ObjectOrder:
# \param order List of indices in which to print objects, ordered by printing # \param order List of indices in which to print objects, ordered by printing
# order. # order.
# \param todo: List of indices which are not yet inserted into the order list. # \param todo: List of indices which are not yet inserted into the order list.
def __init__(self, order: List[SceneNode], todo: List[SceneNode]): def __init__(self, order: List[SceneNode], todo: List[SceneNode]) -> None:
self.order = order self.order = order
self.todo = todo self.todo = todo

View File

@ -1,26 +1,27 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Math.Vector import Vector
from UM.Operations.Operation import Operation from UM.Operations.Operation import Operation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
## A specialised operation designed specifically to modify the previous operation. ## A specialised operation designed specifically to modify the previous operation.
class PlatformPhysicsOperation(Operation): class PlatformPhysicsOperation(Operation):
def __init__(self, node, translation): def __init__(self, node: SceneNode, translation: Vector) -> None:
super().__init__() super().__init__()
self._node = node self._node = node
self._old_transformation = node.getLocalTransformation() self._old_transformation = node.getLocalTransformation()
self._translation = translation self._translation = translation
self._always_merge = True self._always_merge = True
def undo(self): def undo(self) -> None:
self._node.setTransformation(self._old_transformation) self._node.setTransformation(self._old_transformation)
def redo(self): def redo(self) -> None:
self._node.translate(self._translation, SceneNode.TransformSpace.World) self._node.translate(self._translation, SceneNode.TransformSpace.World)
def mergeWith(self, other): def mergeWith(self, other: Operation) -> GroupedOperation:
group = GroupedOperation() group = GroupedOperation()
group.addOperation(other) group.addOperation(other)
@ -28,5 +29,5 @@ class PlatformPhysicsOperation(Operation):
return group return group
def __repr__(self): def __repr__(self) -> str:
return "PlatformPhysicsOp.(trans.={0})".format(self._translation) return "PlatformPhysicsOp.(trans.={0})".format(self._translation)

View File

@ -6,9 +6,9 @@ from UM.Operations.Operation import Operation
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
## Simple operation to set the buildplate number of a scenenode. ## Simple operation to set the buildplate number of a scenenode.
class SetBuildPlateNumberOperation(Operation): class SetBuildPlateNumberOperation(Operation):
def __init__(self, node: SceneNode, build_plate_nr: int) -> None: def __init__(self, node: SceneNode, build_plate_nr: int) -> None:
super().__init__() super().__init__()
self._node = node self._node = node
@ -16,11 +16,11 @@ class SetBuildPlateNumberOperation(Operation):
self._previous_build_plate_nr = None self._previous_build_plate_nr = None
self._decorator_added = False self._decorator_added = False
def undo(self): def undo(self) -> None:
if self._previous_build_plate_nr: if self._previous_build_plate_nr:
self._node.callDecoration("setBuildPlateNumber", self._previous_build_plate_nr) self._node.callDecoration("setBuildPlateNumber", self._previous_build_plate_nr)
def redo(self): def redo(self) -> None:
stack = self._node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway. stack = self._node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
if not stack: if not stack:
self._node.addDecorator(SettingOverrideDecorator()) self._node.addDecorator(SettingOverrideDecorator())

View File

@ -1,36 +1,37 @@
# Copyright (c) 2016 Ultimaker B.V. # Copyright (c) 2016 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher. # Uranium is released under the terms of the LGPLv3 or higher.
from typing import Optional
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Operations import Operation from UM.Operations import Operation
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
## An operation that parents a scene node to another scene node.
## An operation that parents a scene node to another scene node.
class SetParentOperation(Operation.Operation): class SetParentOperation(Operation.Operation):
## Initialises this SetParentOperation. ## Initialises this SetParentOperation.
# #
# \param node The node which will be reparented. # \param node The node which will be reparented.
# \param parent_node The node which will be the parent. # \param parent_node The node which will be the parent.
def __init__(self, node, parent_node): def __init__(self, node: SceneNode, parent_node: Optional[SceneNode]) -> None:
super().__init__() super().__init__()
self._node = node self._node = node
self._parent = parent_node self._parent = parent_node
self._old_parent = node.getParent() # To restore the previous parent in case of an undo. self._old_parent = node.getParent() # To restore the previous parent in case of an undo.
## Undoes the set-parent operation, restoring the old parent. ## Undoes the set-parent operation, restoring the old parent.
def undo(self): def undo(self) -> None:
self._set_parent(self._old_parent) self._set_parent(self._old_parent)
## Re-applies the set-parent operation. ## Re-applies the set-parent operation.
def redo(self): def redo(self) -> None:
self._set_parent(self._parent) self._set_parent(self._parent)
## Sets the parent of the node while applying transformations to the world-transform of the node stays the same. ## Sets the parent of the node while applying transformations to the world-transform of the node stays the same.
# #
# \param new_parent The new parent. Note: this argument can be None, which would hide the node from the scene. # \param new_parent The new parent. Note: this argument can be None, which would hide the node from the scene.
def _set_parent(self, new_parent): def _set_parent(self, new_parent: Optional[SceneNode]) -> None:
if new_parent: if new_parent:
current_parent = self._node.getParent() current_parent = self._node.getParent()
if current_parent: if current_parent:
@ -59,5 +60,5 @@ class SetParentOperation(Operation.Operation):
## Returns a programmer-readable representation of this operation. ## Returns a programmer-readable representation of this operation.
# #
# \return A programmer-readable representation of this operation. # \return A programmer-readable representation of this operation.
def __repr__(self): def __repr__(self) -> str:
return "SetParentOperation(node = {0}, parent_node={1})".format(self._node, self._parent) return "SetParentOperation(node = {0}, parent_node={1})".format(self._node, self._parent)

View File

@ -1,9 +1,11 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
from shapely.errors import TopologicalError # To capture errors if Shapely messes up.
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
@ -136,7 +138,11 @@ class PlatformPhysics:
own_convex_hull = node.callDecoration("getConvexHull") own_convex_hull = node.callDecoration("getConvexHull")
other_convex_hull = other_node.callDecoration("getConvexHull") other_convex_hull = other_node.callDecoration("getConvexHull")
if own_convex_hull and other_convex_hull: if own_convex_hull and other_convex_hull:
try:
overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull) overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
except TopologicalError as e: # Can happen if the convex hull is degenerate?
Logger.warning("Got a topological error when calculating convex hull intersection: {err}".format(err = str(e)))
overlap = False
if overlap: # Moving ensured that overlap was still there. Try anew! if overlap: # Moving ensured that overlap was still there. Try anew!
temp_move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor, temp_move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
z = move_vector.z + overlap[1] * self._move_factor) z = move_vector.z + overlap[1] * self._move_factor)

View File

@ -17,9 +17,6 @@ from cura.Scene.CuraSceneNode import CuraSceneNode
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.View.GL.ShaderProgram import ShaderProgram from UM.View.GL.ShaderProgram import ShaderProgram
MYPY = False
if MYPY:
from UM.Scene.Camera import Camera from UM.Scene.Camera import Camera

View File

@ -3,6 +3,7 @@ from PyQt5.QtQuick import QQuickImageProvider
from PyQt5.QtCore import QSize from PyQt5.QtCore import QSize
from UM.Application import Application from UM.Application import Application
from typing import Tuple
class PrintJobPreviewImageProvider(QQuickImageProvider): class PrintJobPreviewImageProvider(QQuickImageProvider):
@ -10,7 +11,7 @@ class PrintJobPreviewImageProvider(QQuickImageProvider):
super().__init__(QQuickImageProvider.Image) super().__init__(QQuickImageProvider.Image)
## Request a new image. ## Request a new image.
def requestImage(self, id: str, size: QSize) -> QImage: def requestImage(self, id: str, size: QSize) -> Tuple[QImage, QSize]:
# The id will have an uuid and an increment separated by a slash. As we don't care about the value of the # The id will have an uuid and an increment separated by a slash. As we don't care about the value of the
# increment, we need to strip that first. # increment, we need to strip that first.
uuid = id[id.find("/") + 1:] uuid = id[id.find("/") + 1:]
@ -22,6 +23,6 @@ class PrintJobPreviewImageProvider(QQuickImageProvider):
if print_job.key == uuid: if print_job.key == uuid:
if print_job.getPreviewImage(): if print_job.getPreviewImage():
return print_job.getPreviewImage(), QSize(15, 15) return print_job.getPreviewImage(), QSize(15, 15)
else:
return QImage(), QSize(15, 15) return QImage(), QSize(15, 15)
return QImage(), QSize(15, 15) return QImage(), QSize(15, 15)

View File

@ -33,6 +33,10 @@ class FirmwareUpdater(QObject):
else: else:
self._firmware_file = firmware_file self._firmware_file = firmware_file
if self._firmware_file == "":
self._setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error)
return
self._setFirmwareUpdateState(FirmwareUpdateState.updating) self._setFirmwareUpdateState(FirmwareUpdateState.updating)
self._update_firmware_thread.start() self._update_firmware_thread.start()

View File

@ -161,7 +161,7 @@ class PrintJobOutputModel(QObject):
self._time_elapsed = new_time_elapsed self._time_elapsed = new_time_elapsed
self.timeElapsedChanged.emit() self.timeElapsedChanged.emit()
def updateState(self, new_state): def updateState(self, new_state: str) -> None:
if self._state != new_state: if self._state != new_state:
self._state = new_state self._state = new_state
self.stateChanged.emit() self.stateChanged.emit()

View File

@ -148,7 +148,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
@pyqtProperty(QObject, notify = printersChanged) @pyqtProperty(QObject, notify = printersChanged)
def activePrinter(self) -> Optional["PrinterOutputModel"]: def activePrinter(self) -> Optional["PrinterOutputModel"]:
if len(self._printers): if self._printers:
return self._printers[0] return self._printers[0]
return None return None

View File

@ -10,3 +10,6 @@ class BlockSlicingDecorator(SceneNodeDecorator):
def isBlockSlicing(self) -> bool: def isBlockSlicing(self) -> bool:
return True return True
def __deepcopy__(self, memo):
return BlockSlicingDecorator()

View File

@ -36,8 +36,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
# Make sure the timer is created on the main thread # Make sure the timer is created on the main thread
self._recompute_convex_hull_timer = None # type: Optional[QTimer] self._recompute_convex_hull_timer = None # type: Optional[QTimer]
self._timer_scheduled_to_be_created = False
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
if CuraApplication.getInstance() is not None: if CuraApplication.getInstance() is not None:
self._timer_scheduled_to_be_created = True
CuraApplication.getInstance().callLater(self.createRecomputeConvexHullTimer) CuraApplication.getInstance().callLater(self.createRecomputeConvexHullTimer)
self._raft_thickness = 0.0 self._raft_thickness = 0.0
@ -171,7 +173,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
if self._recompute_convex_hull_timer is not None: if self._recompute_convex_hull_timer is not None:
self._recompute_convex_hull_timer.start() self._recompute_convex_hull_timer.start()
else: else:
self.recomputeConvexHull() from cura.CuraApplication import CuraApplication
if not self._timer_scheduled_to_be_created:
# The timer is not created and we never scheduled it. Time to create it now!
CuraApplication.getInstance().callLater(self.createRecomputeConvexHullTimer)
# Now we know for sure that the timer has been scheduled for creation, so we can try this again.
CuraApplication.getInstance().callLater(self.recomputeConvexHullDelayed)
def recomputeConvexHull(self) -> None: def recomputeConvexHull(self) -> None:
controller = Application.getInstance().getController() controller = Application.getInstance().getController()

View File

@ -17,8 +17,8 @@ class GCodeListDecorator(SceneNodeDecorator):
def getGCodeList(self) -> List[str]: def getGCodeList(self) -> List[str]:
return self._gcode_list return self._gcode_list
def setGCodeList(self, list: List[str]) -> None: def setGCodeList(self, gcode_list: List[str]) -> None:
self._gcode_list = list self._gcode_list = gcode_list
def __deepcopy__(self, memo) -> "GCodeListDecorator": def __deepcopy__(self, memo) -> "GCodeListDecorator":
copied_decorator = GCodeListDecorator() copied_decorator = GCodeListDecorator()

View File

@ -239,6 +239,8 @@ class ContainerManager(QObject):
container_type = container_registry.getContainerForMimeType(mime_type) container_type = container_registry.getContainerForMimeType(mime_type)
if not container_type: if not container_type:
return {"status": "error", "message": "Could not find a container to handle the specified file."} return {"status": "error", "message": "Could not find a container to handle the specified file."}
if not issubclass(container_type, InstanceContainer):
return {"status": "error", "message": "This is not a material container, but another type of file."}
container_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_url))) container_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_url)))
container_id = container_registry.uniqueName(container_id) container_id = container_registry.uniqueName(container_id)

View File

@ -15,7 +15,6 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingInstance import SettingInstance from UM.Settings.SettingInstance import SettingInstance
from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.Platform import Platform from UM.Platform import Platform
@ -176,7 +175,7 @@ class CuraContainerRegistry(ContainerRegistry):
if not file_name: if not file_name:
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")} return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")}
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack: if not global_stack:
return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Can't import profile from <filename>{0}</filename> before a printer is added.", file_name)} return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Can't import profile from <filename>{0}</filename> before a printer is added.", file_name)}
container_tree = ContainerTree.getInstance() container_tree = ContainerTree.getInstance()
@ -384,7 +383,7 @@ class CuraContainerRegistry(ContainerRegistry):
if not quality_type: if not quality_type:
return catalog.i18nc("@info:status", "Profile is missing a quality type.") return catalog.i18nc("@info:status", "Profile is missing a quality type.")
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None: if global_stack is None:
return None return None
definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition

View File

@ -133,6 +133,38 @@ class CuraFormulaFunctions:
context = self.createContextForDefaultValueEvaluation(global_stack) context = self.createContextForDefaultValueEvaluation(global_stack)
return self.getResolveOrValue(property_key, context = context) return self.getResolveOrValue(property_key, context = context)
# Gets the value for the given setting key starting from the given container index.
def getValueFromContainerAtIndex(self, property_key: str, container_index: int,
context: Optional["PropertyEvaluationContext"] = None) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
context = self.createContextForDefaultValueEvaluation(global_stack)
context.context["evaluate_from_container_index"] = container_index
return global_stack.getProperty(property_key, "value", context = context)
# Gets the extruder value for the given setting key starting from the given container index.
def getValueFromContainerAtIndexInExtruder(self, extruder_position: int, property_key: str, container_index: int,
context: Optional["PropertyEvaluationContext"] = None) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
if extruder_position == -1:
extruder_position = int(machine_manager.defaultExtruderPosition)
global_stack = machine_manager.activeMachine
try:
extruder_stack = global_stack.extruderList[int(extruder_position)]
except IndexError:
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available. " % (property_key, extruder_position))
return None
context = self.createContextForDefaultValueEvaluation(extruder_stack)
context.context["evaluate_from_container_index"] = container_index
return self.getValueInExtruder(extruder_position, property_key, context)
# Creates a context for evaluating default values (skip the user_changes container). # Creates a context for evaluating default values (skip the user_changes container).
def createContextForDefaultValueEvaluation(self, source_stack: "CuraContainerStack") -> "PropertyEvaluationContext": def createContextForDefaultValueEvaluation(self, source_stack: "CuraContainerStack") -> "PropertyEvaluationContext":
context = PropertyEvaluationContext(source_stack) context = PropertyEvaluationContext(source_stack)

View File

@ -58,7 +58,10 @@ class CuraStackBuilder:
# Create ExtruderStacks # Create ExtruderStacks
extruder_dict = machine_definition.getMetaDataEntry("machine_extruder_trains") extruder_dict = machine_definition.getMetaDataEntry("machine_extruder_trains")
for position in extruder_dict: for position in extruder_dict:
try:
cls.createExtruderStackWithDefaultSetup(new_global_stack, position) cls.createExtruderStackWithDefaultSetup(new_global_stack, position)
except IndexError:
return None
for new_extruder in new_global_stack.extruders.values(): # Only register the extruders if we're sure that all of them are correct. for new_extruder in new_global_stack.extruders.values(): # Only register the extruders if we're sure that all of them are correct.
registry.addContainer(new_extruder) registry.addContainer(new_extruder)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt. from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
@ -275,6 +275,25 @@ class ExtruderManager(QObject):
Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids) Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids)
return [] return []
## Get the extruder that the print will start with.
#
# This should mirror the implementation in CuraEngine of
# ``FffGcodeWriter::getStartExtruder()``.
def getInitialExtruderNr(self) -> int:
application = cura.CuraApplication.CuraApplication.getInstance()
global_stack = application.getGlobalContainerStack()
# Starts with the adhesion extruder.
if global_stack.getProperty("adhesion_type", "value") != "none":
return global_stack.getProperty("adhesion_extruder_nr", "value")
# No adhesion? Well maybe there is still support brim.
if (global_stack.getProperty("support_enable", "value") or global_stack.getProperty("support_tree_enable", "value")) and global_stack.getProperty("support_brim_enable", "value"):
return global_stack.getProperty("support_infill_extruder_nr", "value")
# REALLY no adhesion? Use the first used extruder.
return self.getUsedExtruderStacks()[0].getProperty("extruder_nr", "value")
## Removes the container stack and user profile for the extruders for a specific machine. ## Removes the container stack and user profile for the extruders for a specific machine.
# #
# \param machine_id The machine to remove the extruders for. # \param machine_id The machine to remove the extruders for.

View File

@ -320,8 +320,19 @@ class MachineManager(QObject):
# This signal might not have been emitted yet (if it didn't change) but we still want the models to update that depend on it because we changed the contents of the containers too. # This signal might not have been emitted yet (if it didn't change) but we still want the models to update that depend on it because we changed the contents of the containers too.
extruder_manager.activeExtruderChanged.emit() extruder_manager.activeExtruderChanged.emit()
# Validate if the machine has the correct variants
# It can happen that a variant is empty, even though the machine has variants. This will ensure that that
# that situation will be fixed (and not occur again, since it switches it out to the preferred variant instead!)
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
for extruder in self._global_container_stack.extruderList:
variant_name = extruder.variant.getName()
variant_node = machine_node.variants.get(variant_name)
if variant_node is None:
Logger.log("w", "An extruder has an unknown variant, switching it to the preferred variant")
self.setVariantByName(extruder.getMetaDataEntry("position"), machine_node.preferred_variant_name)
self.__emitChangedSignals() self.__emitChangedSignals()
## Given a definition id, return the machine with this id. ## Given a definition id, return the machine with this id.
# Optional: add a list of keys and values to filter the list of machines with the given definition id # Optional: add a list of keys and values to filter the list of machines with the given definition id
# \param definition_id \type{str} definition id that needs to look for # \param definition_id \type{str} definition id that needs to look for
@ -336,9 +347,9 @@ class MachineManager(QObject):
return cast(GlobalStack, machine) return cast(GlobalStack, machine)
return None return None
@pyqtSlot(str) @pyqtSlot(str, result=bool)
@pyqtSlot(str, str) @pyqtSlot(str, str, result = bool)
def addMachine(self, definition_id: str, name: Optional[str] = None) -> None: def addMachine(self, definition_id: str, name: Optional[str] = None) -> bool:
Logger.log("i", "Trying to add a machine with the definition id [%s]", definition_id) Logger.log("i", "Trying to add a machine with the definition id [%s]", definition_id)
if name is None: if name is None:
definitions = CuraContainerRegistry.getInstance().findDefinitionContainers(id = definition_id) definitions = CuraContainerRegistry.getInstance().findDefinitionContainers(id = definition_id)
@ -353,6 +364,8 @@ class MachineManager(QObject):
self.setActiveMachine(new_stack.getId()) self.setActiveMachine(new_stack.getId())
else: else:
Logger.log("w", "Failed creating a new machine!") Logger.log("w", "Failed creating a new machine!")
return False
return True
def _checkStacksHaveErrors(self) -> bool: def _checkStacksHaveErrors(self) -> bool:
time_start = time.time() time_start = time.time()
@ -671,7 +684,10 @@ class MachineManager(QObject):
if other_machine_stacks: if other_machine_stacks:
self.setActiveMachine(other_machine_stacks[0]["id"]) self.setActiveMachine(other_machine_stacks[0]["id"])
metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0] metadatas = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)
if not metadatas:
return # machine_id doesn't exist. Nothing to remove.
metadata = metadatas[0]
ExtruderManager.getInstance().removeMachineExtruders(machine_id) ExtruderManager.getInstance().removeMachineExtruders(machine_id)
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id) containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
for container in containers: for container in containers:
@ -747,6 +763,11 @@ class MachineManager(QObject):
result = [] # type: List[str] result = [] # type: List[str]
for setting_instance in container.findInstances(): for setting_instance in container.findInstances():
setting_key = setting_instance.definition.key setting_key = setting_instance.definition.key
if setting_key == "print_sequence":
old_value = container.getProperty(setting_key, "value")
Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value)
result.append(setting_key)
continue
if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"): if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"):
continue continue
@ -795,7 +816,7 @@ class MachineManager(QObject):
definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count) definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count)
self.updateDefaultExtruder() self.updateDefaultExtruder()
self.updateNumberExtrudersEnabled() self.numberExtrudersEnabledChanged.emit()
self.correctExtruderSettings() self.correctExtruderSettings()
# Check to see if any objects are set to print with an extruder that will no longer exist # Check to see if any objects are set to print with an extruder that will no longer exist
@ -1502,7 +1523,17 @@ class MachineManager(QObject):
if quality_id == empty_quality_container.getId(): if quality_id == empty_quality_container.getId():
extruder.intent = empty_intent_container extruder.intent = empty_intent_container
continue continue
quality_node = container_tree.machines[definition_id].variants[variant_name].materials[material_base_file].qualities[quality_id]
# Yes, we can find this in a single line of code. This makes it easier to read and it has the benefit
# that it doesn't lump key errors together for the crashlogs
try:
machine_node = container_tree.machines[definition_id]
variant_node = machine_node.variants[variant_name]
material_node = variant_node.materials[material_base_file]
quality_node = material_node.qualities[quality_id]
except KeyError as e:
Logger.error("Can't set the intent category '{category}' since the profile '{profile}' in the stack is not supported according to the container tree.".format(category = intent_category, profile = e))
continue
for intent_node in quality_node.intents.values(): for intent_node in quality_node.intents.values():
if intent_node.intent_category == intent_category: # Found an intent with the correct category. if intent_node.intent_category == intent_category: # Found an intent with the correct category.

View File

@ -43,7 +43,7 @@ class MachineActionManager(QObject):
# Dict of all actions that need to be done when first added by definition ID # Dict of all actions that need to be done when first added by definition ID
self._first_start_actions = {} # type: Dict[str, List[MachineAction]] self._first_start_actions = {} # type: Dict[str, List[MachineAction]]
def initialize(self): def initialize(self) -> None:
# Add machine_action as plugin type # Add machine_action as plugin type
PluginRegistry.addType("machine_action", self.addMachineAction) PluginRegistry.addType("machine_action", self.addMachineAction)

View File

@ -28,7 +28,11 @@ class TextManager(QObject):
def _loadChangeLogText(self) -> str: def _loadChangeLogText(self) -> str:
# Load change log texts and organize them with a dict # Load change log texts and organize them with a dict
try:
file_path = Resources.getPath(Resources.Texts, "change_log.txt") file_path = Resources.getPath(Resources.Texts, "change_log.txt")
except FileNotFoundError:
# I have no idea how / when this happens, but we're getting crash reports about it.
return ""
change_logs_dict = {} # type: Dict[Version, Dict[str, List[str]]] change_logs_dict = {} # type: Dict[Version, Dict[str, List[str]]]
with open(file_path, "r", encoding = "utf-8") as f: with open(file_path, "r", encoding = "utf-8") as f:
open_version = None # type: Optional[Version] open_version = None # type: Optional[Version]

View File

@ -0,0 +1,31 @@
from PyQt5.QtNetwork import QNetworkRequest
from UM.Logger import Logger
from UM.TaskManagement.HttpRequestScope import DefaultUserAgentScope
from cura.API import Account
from cura.CuraApplication import CuraApplication
class UltimakerCloudScope(DefaultUserAgentScope):
"""Add an Authorization header to the request for Ultimaker Cloud Api requests.
When the user is not logged in or a token is not available, a warning will be logged
Also add the user agent headers (see DefaultUserAgentScope)
"""
def __init__(self, application: CuraApplication):
super().__init__(application)
api = application.getCuraAPI()
self._account = api.account # type: Account
def requestHook(self, request: QNetworkRequest):
super().requestHook(request)
token = self._account.accessToken
if not self._account.isLoggedIn or token is None:
Logger.warning("Cannot add authorization to Cloud Api request")
return
header_dict = {
"Authorization": "Bearer {}".format(token)
}
self.addHeaders(request, header_dict)

View File

View File

@ -1,16 +1,34 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
# Remove the working directory from sys.path.
# This fixes a security issue where Cura could import Python packages from the
# current working directory, and therefore be made to execute locally installed
# code (e.g. in the user's home directory where AppImages by default run from).
# See issue CURA-7081.
import sys
if "" in sys.path:
sys.path.remove("")
import argparse import argparse
import faulthandler import faulthandler
import os import os
import sys
# Workaround for a race condition on certain systems where there
# is a race condition between Arcus and PyQt. Importing Arcus
# first seems to prevent Sip from going into a state where it
# tries to create PyQt objects on a non-main thread.
import Arcus # @UnusedImport
import Savitar # @UnusedImport
from PyQt5.QtNetwork import QSslConfiguration, QSslSocket
from UM.Platform import Platform from UM.Platform import Platform
from cura import ApplicationMetadata from cura import ApplicationMetadata
from cura.ApplicationMetadata import CuraAppName from cura.ApplicationMetadata import CuraAppName
from cura.CrashHandler import CrashHandler
try: try:
import sentry_sdk import sentry_sdk
@ -29,21 +47,30 @@ parser.add_argument("--debug",
known_args = vars(parser.parse_known_args()[0]) known_args = vars(parser.parse_known_args()[0])
if with_sentry_sdk: if with_sentry_sdk:
sentry_env = "production" sentry_env = "unknown" # Start off with a "IDK"
if hasattr(sys, "frozen"):
sentry_env = "production" # A frozen build has the posibility to be a "real" distribution.
if ApplicationMetadata.CuraVersion == "master": if ApplicationMetadata.CuraVersion == "master":
sentry_env = "development" sentry_env = "development" # Master is always a development version.
elif ApplicationMetadata.CuraVersion in ["beta", "BETA"]:
sentry_env = "beta"
try: try:
if ApplicationMetadata.CuraVersion.split(".")[2] == "99": if ApplicationMetadata.CuraVersion.split(".")[2] == "99":
sentry_env = "nightly" sentry_env = "nightly"
except IndexError: except IndexError:
pass pass
# Errors to be ignored by Sentry
ignore_errors = [KeyboardInterrupt, MemoryError]
sentry_sdk.init("https://5034bf0054fb4b889f82896326e79b13@sentry.io/1821564", sentry_sdk.init("https://5034bf0054fb4b889f82896326e79b13@sentry.io/1821564",
before_send = CrashHandler.sentryBeforeSend,
environment = sentry_env, environment = sentry_env,
release = "cura%s" % ApplicationMetadata.CuraVersion, release = "cura%s" % ApplicationMetadata.CuraVersion,
default_integrations = False, default_integrations = False,
max_breadcrumbs = 300, max_breadcrumbs = 300,
server_name = "cura") server_name = "cura",
ignore_errors = ignore_errors)
if not known_args["debug"]: if not known_args["debug"]:
def get_cura_dir_path(): def get_cura_dir_path():
@ -156,17 +183,11 @@ def exceptHook(hook_type, value, traceback):
# Set exception hook to use the crash dialog handler # Set exception hook to use the crash dialog handler
sys.excepthook = exceptHook sys.excepthook = exceptHook
# Enable dumping traceback for all threads # Enable dumping traceback for all threads
if sys.stderr: if sys.stderr and not sys.stderr.closed:
faulthandler.enable(file = sys.stderr, all_threads = True) faulthandler.enable(file = sys.stderr, all_threads = True)
else: elif sys.stdout and not sys.stdout.closed:
faulthandler.enable(file = sys.stdout, all_threads = True) faulthandler.enable(file = sys.stdout, all_threads = True)
# Workaround for a race condition on certain systems where there
# is a race condition between Arcus and PyQt. Importing Arcus
# first seems to prevent Sip from going into a state where it
# tries to create PyQt objects on a non-main thread.
import Arcus #@UnusedImport
import Savitar #@UnusedImport
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
@ -201,5 +222,10 @@ if Platform.isLinux() and getattr(sys, "frozen", False):
import trimesh.exchange.load import trimesh.exchange.load
os.environ["LD_LIBRARY_PATH"] = old_env os.environ["LD_LIBRARY_PATH"] = old_env
if ApplicationMetadata.CuraDebugMode:
ssl_conf = QSslConfiguration.defaultConfiguration()
ssl_conf.setPeerVerifyMode(QSslSocket.VerifyNone)
QSslConfiguration.setDefaultConfiguration(ssl_conf)
app = CuraApplication() app = CuraApplication()
app.run() app.run()

View File

@ -13,28 +13,46 @@ export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}"
cd "${PROJECT_DIR}" cd "${PROJECT_DIR}"
# #
# Clone Uranium and set PYTHONPATH first # Clone Uranium and set PYTHONPATH first
# #
# Check the branch to use: # Check the branch to use for Uranium.
# 1. Use the Uranium branch with the branch same if it exists. # It tries the following branch names and uses the first one that's available.
# 2. Otherwise, use the default branch name "master" # - GITHUB_HEAD_REF: the branch name of a PR. If it's not a PR, it will be empty.
# - GITHUB_BASE_REF: the branch a PR is based on. If it's not a PR, it will be empty.
# - GITHUB_REF: the branch name if it's a branch on the repository;
# refs/pull/123/merge if it's a pull_request.
# - master: the master branch. It should always exist.
# For debugging.
echo "GITHUB_REF: ${GITHUB_REF}" echo "GITHUB_REF: ${GITHUB_REF}"
echo "GITHUB_HEAD_REF: ${GITHUB_HEAD_REF}"
echo "GITHUB_BASE_REF: ${GITHUB_BASE_REF}" echo "GITHUB_BASE_REF: ${GITHUB_BASE_REF}"
GIT_REF_NAME="${GITHUB_REF}" GIT_REF_NAME_LIST=( "${GITHUB_HEAD_REF}" "${GITHUB_BASE_REF}" "${GITHUB_REF}" "master" )
if [ -n "${GITHUB_BASE_REF}" ]; then for git_ref_name in "${GIT_REF_NAME_LIST[@]}"
GIT_REF_NAME="${GITHUB_BASE_REF}" do
if [ -z "${git_ref_name}" ]; then
continue
fi fi
GIT_REF_NAME="$(basename "${GIT_REF_NAME}")" git_ref_name="$(basename "${git_ref_name}")"
# Skip refs/pull/1234/merge as pull requests use it as GITHUB_REF
URANIUM_BRANCH="${GIT_REF_NAME:-master}" if [[ "${git_ref_name}" == "merge" ]]; then
echo "Skip [${git_ref_name}]"
continue
fi
URANIUM_BRANCH="${git_ref_name}"
output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")" output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")"
if [ -z "${output}" ]; then if [ -n "${output}" ]; then
echo "Could not find Uranium banch ${URANIUM_BRANCH}, fallback to use master." echo "Found Uranium branch [${URANIUM_BRANCH}]."
URANIUM_BRANCH="master" break
else
echo "Could not find Uranium banch [${URANIUM_BRANCH}], try next."
fi fi
done
echo "Using Uranium branch ${URANIUM_BRANCH} ..." echo "Using Uranium branch ${URANIUM_BRANCH} ..."
git clone --depth=1 -b "${URANIUM_BRANCH}" https://github.com/Ultimaker/Uranium.git "${PROJECT_DIR}"/Uranium git clone --depth=1 -b "${URANIUM_BRANCH}" https://github.com/Ultimaker/Uranium.git "${PROJECT_DIR}"/Uranium

View File

@ -52,7 +52,6 @@ class ThreeMFReader(MeshReader):
self._root = None self._root = None
self._base_name = "" self._base_name = ""
self._unit = None self._unit = None
self._object_count = 0 # Used to name objects as there is no node name yet.
def _createMatrixFromTransformationString(self, transformation: str) -> Matrix: def _createMatrixFromTransformationString(self, transformation: str) -> Matrix:
if transformation == "": if transformation == "":
@ -86,15 +85,21 @@ class ThreeMFReader(MeshReader):
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node. ## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
# \returns Scene node. # \returns Scene node.
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode) -> Optional[SceneNode]: def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
self._object_count += 1 node_name = savitar_node.getName()
node_name = "Object %s" % self._object_count node_id = savitar_node.getId()
if node_name == "":
if file_name != "":
node_name = os.path.basename(file_name)
else:
node_name = "Object {}".format(node_id)
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
um_node = CuraSceneNode() # This adds a SettingOverrideDecorator um_node = CuraSceneNode() # This adds a SettingOverrideDecorator
um_node.addDecorator(BuildPlateDecorator(active_build_plate)) um_node.addDecorator(BuildPlateDecorator(active_build_plate))
um_node.setName(node_name) um_node.setName(node_name)
um_node.setId(node_id)
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation()) transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
um_node.setTransformation(transformation) um_node.setTransformation(transformation)
mesh_builder = MeshBuilder() mesh_builder = MeshBuilder()
@ -104,6 +109,10 @@ class ThreeMFReader(MeshReader):
vertices = numpy.resize(data, (int(data.size / 3), 3)) vertices = numpy.resize(data, (int(data.size / 3), 3))
mesh_builder.setVertices(vertices) mesh_builder.setVertices(vertices)
mesh_builder.calculateNormals(fast=True) mesh_builder.calculateNormals(fast=True)
if file_name:
# The filename is used to give the user the option to reload the file if it is changed on disk
# It is only set for the root node of the 3mf file
mesh_builder.setFileName(file_name)
mesh_data = mesh_builder.build() mesh_data = mesh_builder.build()
if len(mesh_data.getVertices()): if len(mesh_data.getVertices()):
@ -162,7 +171,6 @@ class ThreeMFReader(MeshReader):
def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]: def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
result = [] result = []
self._object_count = 0 # Used to name objects as there is no node name yet.
# The base object of 3mf is a zipped archive. # The base object of 3mf is a zipped archive.
try: try:
archive = zipfile.ZipFile(file_name, "r") archive = zipfile.ZipFile(file_name, "r")
@ -171,7 +179,7 @@ class ThreeMFReader(MeshReader):
scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read()) scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read())
self._unit = scene_3mf.getUnit() self._unit = scene_3mf.getUnit()
for node in scene_3mf.getSceneNodes(): for node in scene_3mf.getSceneNodes():
um_node = self._convertSavitarNodeToUMNode(node) um_node = self._convertSavitarNodeToUMNode(node, file_name)
if um_node is None: if um_node is None:
continue continue
# compensate for original center position, if object(s) is/are not around its zero position # compensate for original center position, if object(s) is/are not around its zero position

View File

@ -4,7 +4,8 @@
from configparser import ConfigParser from configparser import ConfigParser
import zipfile import zipfile
import os import os
from typing import cast, Dict, List, Optional, Tuple import json
from typing import cast, Dict, List, Optional, Tuple, Any
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -284,13 +285,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
serialized = archive.open(instance_container_file_name).read().decode("utf-8") serialized = archive.open(instance_container_file_name).read().decode("utf-8")
# Qualities and variants don't have upgrades, so don't upgrade them # Qualities and variants don't have upgrades, so don't upgrade them
parser = ConfigParser(interpolation = None) parser = ConfigParser(interpolation = None, comment_prefixes = ())
parser.read_string(serialized) parser.read_string(serialized)
container_type = parser["metadata"]["type"] container_type = parser["metadata"]["type"]
if container_type not in ("quality", "variant"): if container_type not in ("quality", "variant"):
serialized = InstanceContainer._updateSerialized(serialized, instance_container_file_name) serialized = InstanceContainer._updateSerialized(serialized, instance_container_file_name)
parser = ConfigParser(interpolation = None) parser = ConfigParser(interpolation = None, comment_prefixes = ())
parser.read_string(serialized) parser.read_string(serialized)
container_info = ContainerInfo(instance_container_file_name, serialized, parser) container_info = ContainerInfo(instance_container_file_name, serialized, parser)
instance_container_info_dict[container_id] = container_info instance_container_info_dict[container_id] = container_info
@ -732,7 +733,25 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
base_file_name = os.path.basename(file_name) base_file_name = os.path.basename(file_name)
self.setWorkspaceName(base_file_name) self.setWorkspaceName(base_file_name)
return nodes
return nodes, self._loadMetadata(file_name)
@staticmethod
def _loadMetadata(file_name: str) -> Dict[str, Dict[str, Any]]:
archive = zipfile.ZipFile(file_name, "r")
metadata_files = [name for name in archive.namelist() if name.endswith("plugin_metadata.json")]
result = dict()
for metadata_file in metadata_files:
try:
plugin_id = metadata_file.split("/")[0]
result[plugin_id] = json.loads(archive.open("%s/plugin_metadata.json" % plugin_id).read().decode("utf-8"))
except Exception:
Logger.logException("w", "Unable to retrieve metadata for %s", metadata_file)
return result
def _processQualityChanges(self, global_stack): def _processQualityChanges(self, global_stack):
if self._machine_info.quality_changes_info is None: if self._machine_info.quality_changes_info is None:

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for reading 3MF files.", "description": "Provides support for reading 3MF files.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import configparser import configparser
@ -6,9 +6,12 @@ from io import StringIO
import zipfile import zipfile
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger
from UM.Preferences import Preferences from UM.Preferences import Preferences
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Workspace.WorkspaceWriter import WorkspaceWriter from UM.Workspace.WorkspaceWriter import WorkspaceWriter
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from cura.Utils.Threading import call_on_qt_thread from cura.Utils.Threading import call_on_qt_thread
@ -25,6 +28,8 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
mesh_writer = application.getMeshFileHandler().getWriter("3MFWriter") mesh_writer = application.getMeshFileHandler().getWriter("3MFWriter")
if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace
self.setInformation(catalog.i18nc("@error:zip", "3MF Writer plug-in is corrupt."))
Logger.error("3MF Writer class is unavailable. Can't write workspace.")
return False return False
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it). # Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
@ -37,6 +42,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
global_stack = machine_manager.activeMachine global_stack = machine_manager.activeMachine
try:
# Add global container stack data to the archive. # Add global container stack data to the archive.
self._writeContainerToArchive(global_stack, archive) self._writeContainerToArchive(global_stack, archive)
@ -49,6 +55,10 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
self._writeContainerToArchive(extruder_stack, archive) self._writeContainerToArchive(extruder_stack, archive)
for container in extruder_stack.getContainers(): for container in extruder_stack.getContainers():
self._writeContainerToArchive(container, archive) self._writeContainerToArchive(container, archive)
except PermissionError:
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
Logger.error("No permission to write workspace to this stream.")
return False
# Write preferences to archive # Write preferences to archive
original_preferences = Application.getInstance().getPreferences() #Copy only the preferences that we use to the workspace. original_preferences = Application.getInstance().getPreferences() #Copy only the preferences that we use to the workspace.
@ -59,6 +69,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
preferences_string = StringIO() preferences_string = StringIO()
temp_preferences.writeToFile(preferences_string) temp_preferences.writeToFile(preferences_string)
preferences_file = zipfile.ZipInfo("Cura/preferences.cfg") preferences_file = zipfile.ZipInfo("Cura/preferences.cfg")
try:
archive.writestr(preferences_file, preferences_string.getvalue()) archive.writestr(preferences_file, preferences_string.getvalue())
# Save Cura version # Save Cura version
@ -73,11 +84,29 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
version_config_parser.write(version_file_string) version_config_parser.write(version_file_string)
archive.writestr(version_file, version_file_string.getvalue()) archive.writestr(version_file, version_file_string.getvalue())
self._writePluginMetadataToArchive(archive)
# Close the archive & reset states. # Close the archive & reset states.
archive.close() archive.close()
except PermissionError:
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
Logger.error("No permission to write workspace to this stream.")
return False
mesh_writer.setStoreArchive(False) mesh_writer.setStoreArchive(False)
return True return True
@staticmethod
def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
file_name_template = "%s/plugin_metadata.json"
for plugin_id, metadata in Application.getInstance().getWorkspaceMetadataStorage().getAllData().items():
file_name = file_name_template % plugin_id
file_in_archive = zipfile.ZipInfo(file_name)
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
import json
archive.writestr(file_in_archive, json.dumps(metadata, separators = (", ", ": "), indent = 4, skipkeys = True))
## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive. ## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.
# \param container That follows the \type{ContainerInterface} to archive. # \param container That follows the \type{ContainerInterface} to archive.
# \param archive The archive to write to. # \param archive The archive to write to.

View File

@ -1,5 +1,6 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2015 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher. # Uranium is released under the terms of the LGPLv3 or higher.
from typing import Optional
from UM.Mesh.MeshWriter import MeshWriter from UM.Mesh.MeshWriter import MeshWriter
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
@ -40,7 +41,7 @@ class ThreeMFWriter(MeshWriter):
} }
self._unit_matrix_string = self._convertMatrixToString(Matrix()) self._unit_matrix_string = self._convertMatrixToString(Matrix())
self._archive = None self._archive = None # type: Optional[zipfile.ZipFile]
self._store_archive = False self._store_archive = False
def _convertMatrixToString(self, matrix): def _convertMatrixToString(self, matrix):
@ -76,6 +77,7 @@ class ThreeMFWriter(MeshWriter):
return return
savitar_node = Savitar.SceneNode() savitar_node = Savitar.SceneNode()
savitar_node.setName(um_node.getName())
node_matrix = um_node.getLocalTransformation() node_matrix = um_node.getLocalTransformation()

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for writing 3MF files.", "description": "Provides support for writing 3MF files.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -118,7 +118,7 @@ class AMFReader(MeshReader):
mesh.merge_vertices() mesh.merge_vertices()
mesh.remove_unreferenced_vertices() mesh.remove_unreferenced_vertices()
mesh.fix_normals() mesh.fix_normals()
mesh_data = self._toMeshData(mesh) mesh_data = self._toMeshData(mesh, file_name)
new_node = CuraSceneNode() new_node = CuraSceneNode()
new_node.setSelectable(True) new_node.setSelectable(True)
@ -147,7 +147,13 @@ class AMFReader(MeshReader):
return group_node return group_node
def _toMeshData(self, tri_node: trimesh.base.Trimesh) -> MeshData: ## Converts a Trimesh to Uranium's MeshData.
# \param tri_node A Trimesh containing the contents of a file that was
# just read.
# \param file_name The full original filename used to watch for changes
# \return Mesh data from the Trimesh in a way that Uranium can understand
# it.
def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData:
tri_faces = tri_node.faces tri_faces = tri_node.faces
tri_vertices = tri_node.vertices tri_vertices = tri_node.vertices
@ -169,5 +175,5 @@ class AMFReader(MeshReader):
indices = numpy.asarray(indices, dtype = numpy.int32) indices = numpy.asarray(indices, dtype = numpy.int32)
normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count) normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count)
mesh_data = MeshData(vertices = vertices, indices = indices, normals = normals) mesh_data = MeshData(vertices = vertices, indices = indices, normals = normals,file_name = file_name)
return mesh_data return mesh_data

View File

@ -3,5 +3,5 @@
"author": "fieldOfView", "author": "fieldOfView",
"version": "1.0.0", "version": "1.0.0",
"description": "Provides support for reading AMF files.", "description": "Provides support for reading AMF files.",
"api": "7.0.0" "api": "7.1.0"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Backup and restore your configuration.", "description": "Backup and restore your configuration.",
"version": "1.2.0", "version": "1.2.0",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -0,0 +1,134 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
import threading
from datetime import datetime
from typing import Any, Dict, Optional
import sentry_sdk
from PyQt5.QtNetwork import QNetworkReply
from UM.Job import Job
from UM.Logger import Logger
from UM.Message import Message
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from UM.i18n import i18nCatalog
from cura.CuraApplication import CuraApplication
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
catalog = i18nCatalog("cura")
class CreateBackupJob(Job):
"""Creates backup zip, requests upload url and uploads the backup file to cloud storage."""
MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
DEFAULT_UPLOAD_ERROR_MESSAGE = catalog.i18nc("@info:backup_status", "There was an error while uploading your backup.")
def __init__(self, api_backup_url: str) -> None:
""" Create a new backup Job. start the job by calling start()
:param api_backup_url: The url of the 'backups' endpoint of the Cura Drive Api
"""
super().__init__()
self._api_backup_url = api_backup_url
self._json_cloud_scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
self._backup_zip = None # type: Optional[bytes]
self._job_done = threading.Event()
"""Set when the job completes. Does not indicate success."""
self.backup_upload_error_message = ""
"""After the job completes, an empty string indicates success. Othrerwise, the value is a translated message."""
def run(self) -> None:
upload_message = Message(catalog.i18nc("@info:backup_status", "Creating your backup..."), title = self.MESSAGE_TITLE, progress = -1)
upload_message.show()
CuraApplication.getInstance().processEvents()
cura_api = CuraApplication.getInstance().getCuraAPI()
self._backup_zip, backup_meta_data = cura_api.backups.createBackup()
if not self._backup_zip or not backup_meta_data:
self.backup_upload_error_message = catalog.i18nc("@info:backup_status", "There was an error while creating your backup.")
upload_message.hide()
return
upload_message.setText(catalog.i18nc("@info:backup_status", "Uploading your backup..."))
CuraApplication.getInstance().processEvents()
# Create an upload entry for the backup.
timestamp = datetime.now().isoformat()
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
self._requestUploadSlot(backup_meta_data, len(self._backup_zip))
self._job_done.wait()
if self.backup_upload_error_message == "":
upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."))
upload_message.setProgress(None) # Hide progress bar
else:
# some error occurred. This error is presented to the user by DrivePluginExtension
upload_message.hide()
def _requestUploadSlot(self, backup_metadata: Dict[str, Any], backup_size: int) -> None:
"""Request a backup upload slot from the API.
:param backup_metadata: A dict containing some meta data about the backup.
:param backup_size: The size of the backup file in bytes.
"""
payload = json.dumps({"data": {"backup_size": backup_size,
"metadata": backup_metadata
}
}).encode()
HttpRequestManager.getInstance().put(
self._api_backup_url,
data = payload,
callback = self._onUploadSlotCompleted,
error_callback = self._onUploadSlotCompleted,
scope = self._json_cloud_scope)
def _onUploadSlotCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if HttpRequestManager.safeHttpStatus(reply) >= 300:
replyText = HttpRequestManager.readText(reply)
Logger.warning("Could not request backup upload: %s", replyText)
self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
if HttpRequestManager.safeHttpStatus(reply) == 400:
errors = json.loads(replyText)["errors"]
if "moreThanMaximum" in [error["code"] for error in errors if error["meta"] and error["meta"]["field_name"] == "backup_size"]:
if self._backup_zip is None: # will never happen; keep mypy happy
zip_error = "backup is None."
else:
zip_error = "{} exceeds max size.".format(str(len(self._backup_zip)))
sentry_sdk.capture_message("backup failed: {}".format(zip_error), level ="warning")
self.backup_upload_error_message = catalog.i18nc("@error:file_size", "The backup exceeds the maximum file size.")
from sentry_sdk import capture_message
self._job_done.set()
return
if error is not None:
Logger.warning("Could not request backup upload: %s", HttpRequestManager.qt_network_error_name(error))
self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
self._job_done.set()
return
backup_upload_url = HttpRequestManager.readJSON(reply)["data"]["upload_url"]
# Upload the backup to storage.
HttpRequestManager.getInstance().put(
backup_upload_url,
data=self._backup_zip,
callback=self._uploadFinishedCallback,
error_callback=self._uploadFinishedCallback
)
def _uploadFinishedCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError = None):
if not HttpRequestManager.replyIndicatesSuccess(reply, error):
Logger.log("w", "Could not upload backup file: %s", HttpRequestManager.readText(reply))
self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
self._job_done.set()

View File

@ -1,90 +1,70 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import base64 from typing import Any, Optional, List, Dict, Callable
import hashlib
from datetime import datetime
from tempfile import NamedTemporaryFile
from typing import Any, Optional, List, Dict
import requests from PyQt5.QtNetwork import QNetworkReply
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message
from UM.Signal import Signal, signalemitter from UM.Signal import Signal, signalemitter
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from UM.i18n import i18nCatalog
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from .UploadBackupJob import UploadBackupJob from .CreateBackupJob import CreateBackupJob
from .RestoreBackupJob import RestoreBackupJob
from .Settings import Settings from .Settings import Settings
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.
@signalemitter @signalemitter
class DriveApiService: class DriveApiService:
"""The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling."""
BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL) BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
# Emit signal when restoring backup started or finished.
restoringStateChanged = Signal() restoringStateChanged = Signal()
"""Emits signal when restoring backup started or finished."""
# Emit signal when creating backup started or finished.
creatingStateChanged = Signal() creatingStateChanged = Signal()
"""Emits signal when creating backup started or finished."""
def __init__(self) -> None: def __init__(self) -> None:
self._cura_api = CuraApplication.getInstance().getCuraAPI() self._cura_api = CuraApplication.getInstance().getCuraAPI()
self._json_cloud_scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
def getBackups(self) -> List[Dict[str, Any]]: def getBackups(self, changed: Callable[[List[Dict[str, Any]]], None]) -> None:
access_token = self._cura_api.account.accessToken def callback(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if not access_token: if error is not None:
Logger.log("w", "Could not get access token.") Logger.log("w", "Could not get backups: " + str(error))
return [] changed([])
try: return
backup_list_request = requests.get(self.BACKUP_URL, headers = {
"Authorization": "Bearer {}".format(access_token)
})
except requests.exceptions.ConnectionError:
Logger.logException("w", "Unable to connect with the server.")
return []
# HTTP status 300s mean redirection. 400s and 500s are errors. backup_list_response = HttpRequestManager.readJSON(reply)
# Technically 300s are not errors, but the use case here relies on "requests" to handle redirects automatically.
if backup_list_request.status_code >= 300:
Logger.log("w", "Could not get backups list from remote: %s", backup_list_request.text)
Message(catalog.i18nc("@info:backup_status", "There was an error listing your backups."), title = catalog.i18nc("@info:title", "Backup")).show()
return []
backup_list_response = backup_list_request.json()
if "data" not in backup_list_response: if "data" not in backup_list_response:
Logger.log("w", "Could not get backups from remote, actual response body was: %s", str(backup_list_response)) Logger.log("w", "Could not get backups from remote, actual response body was: %s",
return [] str(backup_list_response))
changed([]) # empty list of backups
return
return backup_list_response["data"] changed(backup_list_response["data"])
HttpRequestManager.getInstance().get(
self.BACKUP_URL,
callback= callback,
error_callback = callback,
scope=self._json_cloud_scope
)
def createBackup(self) -> None: def createBackup(self) -> None:
self.creatingStateChanged.emit(is_creating = True) self.creatingStateChanged.emit(is_creating = True)
upload_backup_job = CreateBackupJob(self.BACKUP_URL)
# Create the backup.
backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup()
if not backup_zip_file or not backup_meta_data:
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not create backup.")
return
# Create an upload entry for the backup.
timestamp = datetime.now().isoformat()
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file))
if not backup_upload_url:
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not upload backup.")
return
# Upload the backup to storage.
upload_backup_job = UploadBackupJob(backup_upload_url, backup_zip_file)
upload_backup_job.finished.connect(self._onUploadFinished) upload_backup_job.finished.connect(self._onUploadFinished)
upload_backup_job.start() upload_backup_job.start()
def _onUploadFinished(self, job: "UploadBackupJob") -> None: def _onUploadFinished(self, job: "CreateBackupJob") -> None:
if job.backup_upload_error_message != "": if job.backup_upload_error_message != "":
# If the job contains an error message we pass it along so the UI can display it. # If the job contains an error message we pass it along so the UI can display it.
self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message) self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message)
@ -96,96 +76,38 @@ class DriveApiService:
download_url = backup.get("download_url") download_url = backup.get("download_url")
if not download_url: if not download_url:
# If there is no download URL, we can't restore the backup. # If there is no download URL, we can't restore the backup.
return self._emitRestoreError() Logger.warning("backup download_url is missing. Aborting backup.")
try:
download_package = requests.get(download_url, stream = True)
except requests.exceptions.ConnectionError:
Logger.logException("e", "Unable to connect with the server")
return self._emitRestoreError()
if download_package.status_code >= 300:
# Something went wrong when attempting to download the backup.
Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
return self._emitRestoreError()
# We store the file in a temporary path fist to ensure integrity.
temporary_backup_file = NamedTemporaryFile(delete = False)
with open(temporary_backup_file.name, "wb") as write_backup:
for chunk in download_package:
write_backup.write(chunk)
if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash", "")):
# Don't restore the backup if the MD5 hashes do not match.
# This can happen if the download was interrupted.
Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
return self._emitRestoreError()
# Tell Cura to place the backup back in the user data folder.
with open(temporary_backup_file.name, "rb") as read_backup:
self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("metadata", {}))
self.restoringStateChanged.emit(is_restoring = False)
def _emitRestoreError(self) -> None:
self.restoringStateChanged.emit(is_restoring = False, self.restoringStateChanged.emit(is_restoring = False,
error_message = catalog.i18nc("@info:backup_status", error_message = catalog.i18nc("@info:backup_status",
"There was an error trying to restore your backup.")) "There was an error trying to restore your backup."))
return
restore_backup_job = RestoreBackupJob(backup)
restore_backup_job.finished.connect(self._onRestoreFinished)
restore_backup_job.start()
def _onRestoreFinished(self, job: "RestoreBackupJob") -> None:
if job.restore_backup_error_message != "":
# If the job contains an error message we pass it along so the UI can display it.
self.restoringStateChanged.emit(is_restoring=False)
else:
self.restoringStateChanged.emit(is_restoring = False, error_message = job.restore_backup_error_message)
def deleteBackup(self, backup_id: str, finished_callable: Callable[[bool], None]):
def finishedCallback(reply: QNetworkReply, ca: Callable[[bool], None] = finished_callable) -> None:
self._onDeleteRequestCompleted(reply, ca)
def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, ca: Callable[[bool], None] = finished_callable) -> None:
self._onDeleteRequestCompleted(reply, ca, error)
HttpRequestManager.getInstance().delete(
url = "{}/{}".format(self.BACKUP_URL, backup_id),
callback = finishedCallback,
error_callback = errorCallback,
scope= self._json_cloud_scope
)
# Verify the MD5 hash of a file.
# \param file_path Full path to the file.
# \param known_hash The known MD5 hash of the file.
# \return: Success or not.
@staticmethod @staticmethod
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool: def _onDeleteRequestCompleted(reply: QNetworkReply, callable: Callable[[bool], None], error: Optional["QNetworkReply.NetworkError"] = None) -> None:
with open(file_path, "rb") as read_backup: callable(HttpRequestManager.replyIndicatesSuccess(reply, error))
local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8")
return known_hash == local_md5_hash
def deleteBackup(self, backup_id: str) -> bool:
access_token = self._cura_api.account.accessToken
if not access_token:
Logger.log("w", "Could not get access token.")
return False
try:
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
"Authorization": "Bearer {}".format(access_token)
})
except requests.exceptions.ConnectionError:
Logger.logException("e", "Unable to connect with the server")
return False
if delete_backup.status_code >= 300:
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
return False
return True
# Request a backup upload slot from the API.
# \param backup_metadata: A dict containing some meta data about the backup.
# \param backup_size The size of the backup file in bytes.
# \return: The upload URL for the actual backup file if successful, otherwise None.
def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> Optional[str]:
access_token = self._cura_api.account.accessToken
if not access_token:
Logger.log("w", "Could not get access token.")
return None
try:
backup_upload_request = requests.put(
self.BACKUP_URL,
json = {"data": {"backup_size": backup_size,
"metadata": backup_metadata
}
},
headers = {
"Authorization": "Bearer {}".format(access_token)
})
except requests.exceptions.ConnectionError:
Logger.logException("e", "Unable to connect with the server")
return None
# Any status code of 300 or above indicates an error.
if backup_upload_request.status_code >= 300:
Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text)
return None
return backup_upload_request.json()["data"]["upload_url"]

View File

@ -133,7 +133,10 @@ class DrivePluginExtension(QObject, Extension):
@pyqtSlot(name = "refreshBackups") @pyqtSlot(name = "refreshBackups")
def refreshBackups(self) -> None: def refreshBackups(self) -> None:
self._backups = self._drive_api_service.getBackups() self._drive_api_service.getBackups(self._backupsChangedCallback)
def _backupsChangedCallback(self, backups: List[Dict[str, Any]]) -> None:
self._backups = backups
self.backupsChanged.emit() self.backupsChanged.emit()
@pyqtProperty(bool, notify = restoringStateChanged) @pyqtProperty(bool, notify = restoringStateChanged)
@ -158,5 +161,8 @@ class DrivePluginExtension(QObject, Extension):
@pyqtSlot(str, name = "deleteBackup") @pyqtSlot(str, name = "deleteBackup")
def deleteBackup(self, backup_id: str) -> None: def deleteBackup(self, backup_id: str) -> None:
self._drive_api_service.deleteBackup(backup_id) self._drive_api_service.deleteBackup(backup_id, self._backupDeletedCallback)
def _backupDeletedCallback(self, success: bool):
if success:
self.refreshBackups() self.refreshBackups()

View File

@ -0,0 +1,92 @@
import base64
import hashlib
import threading
from tempfile import NamedTemporaryFile
from typing import Optional, Any, Dict
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from UM.Job import Job
from UM.Logger import Logger
from UM.PackageManager import catalog
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from cura.CuraApplication import CuraApplication
class RestoreBackupJob(Job):
"""Downloads a backup and overwrites local configuration with the backup.
When `Job.finished` emits, `restore_backup_error_message` will either be `""` (no error) or an error message
"""
DISK_WRITE_BUFFER_SIZE = 512 * 1024
DEFAULT_ERROR_MESSAGE = catalog.i18nc("@info:backup_status", "There was an error trying to restore your backup.")
def __init__(self, backup: Dict[str, Any]) -> None:
""" Create a new restore Job. start the job by calling start()
:param backup: A dict containing a backup spec
"""
super().__init__()
self._job_done = threading.Event()
self._backup = backup
self.restore_backup_error_message = ""
def run(self) -> None:
url = self._backup.get("download_url")
assert url is not None
HttpRequestManager.getInstance().get(
url = url,
callback = self._onRestoreRequestCompleted,
error_callback = self._onRestoreRequestCompleted
)
self._job_done.wait() # A job is considered finished when the run function completes
def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if not HttpRequestManager.replyIndicatesSuccess(reply, error):
Logger.warning("Requesting backup failed, response code %s while trying to connect to %s",
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
self._job_done.set()
return
# We store the file in a temporary path fist to ensure integrity.
temporary_backup_file = NamedTemporaryFile(delete = False)
with open(temporary_backup_file.name, "wb") as write_backup:
app = CuraApplication.getInstance()
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
while bytes_read:
write_backup.write(bytes_read)
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
app.processEvents()
if not self._verifyMd5Hash(temporary_backup_file.name, self._backup.get("md5_hash", "")):
# Don't restore the backup if the MD5 hashes do not match.
# This can happen if the download was interrupted.
Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
# Tell Cura to place the backup back in the user data folder.
with open(temporary_backup_file.name, "rb") as read_backup:
cura_api = CuraApplication.getInstance().getCuraAPI()
cura_api.backups.restoreBackup(read_backup.read(), self._backup.get("metadata", {}))
self._job_done.set()
@staticmethod
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
"""Verify the MD5 hash of a file.
:param file_path: Full path to the file.
:param known_hash: The known MD5 hash of the file.
:return: Success or not.
"""
with open(file_path, "rb") as read_backup:
local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8")
return known_hash == local_md5_hash

View File

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from cura import UltimakerCloudAuthentication from cura.UltimakerCloud import UltimakerCloudAuthentication
class Settings: class Settings:

View File

@ -1,41 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import requests
from UM.Job import Job
from UM.Logger import Logger
from UM.Message import Message
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class UploadBackupJob(Job):
MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
# This job is responsible for uploading the backup file to cloud storage.
# As it can take longer than some other tasks, we schedule this using a Cura Job.
def __init__(self, signed_upload_url: str, backup_zip: bytes) -> None:
super().__init__()
self._signed_upload_url = signed_upload_url
self._backup_zip = backup_zip
self._upload_success = False
self.backup_upload_error_message = ""
def run(self) -> None:
upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1)
upload_message.show()
backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
upload_message.hide()
if backup_upload.status_code >= 300:
self.backup_upload_error_message = backup_upload.text
Logger.log("w", "Could not upload backup file: %s", backup_upload.text)
Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."), title = self.MESSAGE_TITLE).show()
else:
self._upload_success = True
Message(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."), title = self.MESSAGE_TITLE).show()
self.finished.emit(self)

View File

@ -55,10 +55,22 @@ class CuraEngineBackend(QObject, Backend):
if Platform.isWindows(): if Platform.isWindows():
executable_name += ".exe" executable_name += ".exe"
default_engine_location = executable_name default_engine_location = executable_name
if os.path.exists(os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)):
default_engine_location = os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name) search_path = [
if hasattr(sys, "frozen"): os.path.abspath(os.path.dirname(sys.executable)),
default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name) os.path.abspath(os.path.join(os.path.dirname(sys.executable), "bin")),
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..")),
os.path.join(CuraApplication.getInstallPrefix(), "bin"),
os.path.dirname(os.path.abspath(sys.executable)),
]
for path in search_path:
engine_path = os.path.join(path, executable_name)
if os.path.isfile(engine_path):
default_engine_location = engine_path
break
if Platform.isLinux() and not default_engine_location: if Platform.isLinux() and not default_engine_location:
if not os.getenv("PATH"): if not os.getenv("PATH"):
raise OSError("There is something wrong with your Linux installation.") raise OSError("There is something wrong with your Linux installation.")
@ -409,7 +421,10 @@ class CuraEngineBackend(QObject, Backend):
if job.getResult() == StartJobResult.NothingToSlice: if job.getResult() == StartJobResult.NothingToSlice:
if self._application.platformActivity: if self._application.platformActivity:
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume or are assigned to a disabled extruder. Please scale or rotate models to fit, or enable an extruder."), self._error_message = Message(catalog.i18nc("@info:status", "Please review settings and check if your models:"
"\n- Fit within the build volume"
"\n- Are assigned to an enabled extruder"
"\n- Are not all set as modifier meshes"),
title = catalog.i18nc("@info:title", "Unable to slice")) title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show() self._error_message.show()
self.setState(BackendState.Error) self.setState(BackendState.Error)

View File

@ -1,4 +1,4 @@
# Copyright (c) 2019 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import numpy import numpy
@ -171,7 +171,6 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.ObjectSettingError) self.setResult(StartJobResult.ObjectSettingError)
return return
with self._scene.getSceneLock():
# Remove old layer data. # Remove old layer data.
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number: if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
@ -344,10 +343,7 @@ class StartSliceJob(Job):
result["time"] = time.strftime("%H:%M:%S") #Some extra settings. result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
result["date"] = time.strftime("%d-%m-%Y") result["date"] = time.strftime("%d-%m-%Y")
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
result["initial_extruder_nr"] = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr()
initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
result["initial_extruder_nr"] = initial_extruder_nr
return result return result
@ -426,13 +422,14 @@ class StartSliceJob(Job):
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend # Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
start_gcode = settings["machine_start_gcode"] start_gcode = settings["machine_start_gcode"]
# Remove all the comments from the start g-code
start_gcode = re.sub(r";.+?(\n|$)", "\n", start_gcode)
bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"] bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"]
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr} pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr}
settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None
print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"] print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"]
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr} pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr}
settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None
# Replace the setting tokens in start and end g-code. # Replace the setting tokens in start and end g-code.
# Use values from the first used extruder by default so we get the expected temperatures # Use values from the first used extruder by default so we get the expected temperatures
initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0]

View File

@ -2,7 +2,7 @@
"name": "CuraEngine Backend", "name": "CuraEngine Backend",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"description": "Provides the link to the CuraEngine slicing backend.", "description": "Provides the link to the CuraEngine slicing backend.",
"api": "7.0", "api": "7.1",
"version": "1.0.1", "version": "1.0.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for importing Cura profiles.", "description": "Provides support for importing Cura profiles.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for exporting Cura profiles.", "description": "Provides support for exporting Cura profiles.",
"api": "7.0", "api": "7.1",
"i18n-catalog":"cura" "i18n-catalog":"cura"
} }

View File

@ -44,6 +44,7 @@ class FirmwareUpdateCheckerJob(Job):
try: try:
# CURA-6698 Create an SSL context and use certifi CA certificates for verification. # CURA-6698 Create an SSL context and use certifi CA certificates for verification.
context = ssl.SSLContext(protocol = ssl.PROTOCOL_TLSv1_2) context = ssl.SSLContext(protocol = ssl.PROTOCOL_TLSv1_2)
context.verify_mode = ssl.CERT_REQUIRED
context.load_verify_locations(cafile = certifi.where()) context.load_verify_locations(cafile = certifi.where())
request = urllib.request.Request(url, headers = self._headers) request = urllib.request.Request(url, headers = self._headers)

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Checks for firmware updates.", "description": "Checks for firmware updates.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a machine actions for updating firmware.", "description": "Provides a machine actions for updating firmware.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Reads g-code from a compressed archive.", "description": "Reads g-code from a compressed archive.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Writes g-code to a compressed archive.", "description": "Writes g-code to a compressed archive.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for importing profiles from g-code files.", "description": "Provides support for importing profiles from g-code files.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import math import math
@ -169,6 +169,9 @@ class FlavorParser:
# A threshold is set to avoid weird paths in the GCode # A threshold is set to avoid weird paths in the GCode
if line_width > 1.2: if line_width > 1.2:
return 0.35 return 0.35
# Prevent showing infinitely wide lines
if line_width < 0.0:
return 0.0
return line_width return line_width
def _gCode0(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: def _gCode0(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
@ -235,7 +238,7 @@ class FlavorParser:
def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
if params.e is not None: if params.e is not None:
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e self._extrusion_length_offset[self._extruder_number] = position.e[self._extruder_number] - params.e
position.e[self._extruder_number] = params.e position.e[self._extruder_number] = params.e
self._previous_extrusion_value = params.e self._previous_extrusion_value = params.e
else: else:
@ -258,16 +261,19 @@ class FlavorParser:
continue continue
if item.startswith(";"): if item.startswith(";"):
continue continue
try:
if item[0] == "X": if item[0] == "X":
x = float(item[1:]) x = float(item[1:])
if item[0] == "Y": elif item[0] == "Y":
y = float(item[1:]) y = float(item[1:])
if item[0] == "Z": elif item[0] == "Z":
z = float(item[1:]) z = float(item[1:])
if item[0] == "F": elif item[0] == "F":
f = float(item[1:]) / 60 f = float(item[1:]) / 60
if item[0] == "E": elif item[0] == "E":
e = float(item[1:]) e = float(item[1:])
except ValueError: # Improperly formatted g-code: Coordinates are not floats.
continue # Skip the command then.
params = PositionOptional(x, y, z, f, e) params = PositionOptional(x, y, z, f, e)
return func(position, params, path) return func(position, params, path)
return position return position

View File

@ -1,8 +1,8 @@
{ {
"name": "G-code Reader", "name": "G-code Reader",
"author": "Victor Larchenko, Ultimaker", "author": "Victor Larchenko, Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Allows loading and displaying G-code files.", "description": "Allows loading and displaying G-code files.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Writes g-code to a file.", "description": "Writes g-code to a file.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -1,4 +1,4 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import numpy import numpy

View File

@ -1,4 +1,4 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
@ -33,9 +33,9 @@ class ImageReaderUI(QObject):
self.base_height = 0.4 self.base_height = 0.4
self.peak_height = 2.5 self.peak_height = 2.5
self.smoothing = 1 self.smoothing = 1
self.lighter_is_higher = False; self.lighter_is_higher = False
self.use_transparency_model = True; self.use_transparency_model = True
self.transmittance_1mm = 50.0; # based on pearl PLA self.transmittance_1mm = 50.0 # based on pearl PLA
self._ui_lock = threading.Lock() self._ui_lock = threading.Lock()
self._cancelled = False self._cancelled = False
@ -85,26 +85,37 @@ class ImageReaderUI(QObject):
Logger.log("d", "Creating ImageReader config UI") Logger.log("d", "Creating ImageReader config UI")
path = os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml") path = os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml")
self._ui_view = Application.getInstance().createQmlComponent(path, {"manager": self}) self._ui_view = Application.getInstance().createQmlComponent(path, {"manager": self})
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint); self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint)
self._disable_size_callbacks = False self._disable_size_callbacks = False
@pyqtSlot() @pyqtSlot()
def onOkButtonClicked(self): def onOkButtonClicked(self):
self._cancelled = False self._cancelled = False
self._ui_view.close() self._ui_view.close()
try:
self._ui_lock.release() self._ui_lock.release()
except RuntimeError:
# We don't really care if it was held or not. Just make sure it's not held now
pass
@pyqtSlot() @pyqtSlot()
def onCancelButtonClicked(self): def onCancelButtonClicked(self):
self._cancelled = True self._cancelled = True
self._ui_view.close() self._ui_view.close()
try:
self._ui_lock.release() self._ui_lock.release()
except RuntimeError:
# We don't really care if it was held or not. Just make sure it's not held now
pass
@pyqtSlot(str) @pyqtSlot(str)
def onWidthChanged(self, value): def onWidthChanged(self, value):
if self._ui_view and not self._disable_size_callbacks: if self._ui_view and not self._disable_size_callbacks:
if len(value) > 0: if len(value) > 0:
try:
self._width = float(value.replace(",", ".")) self._width = float(value.replace(",", "."))
except ValueError: # Can happen with incomplete numbers, such as "-".
self._width = 0
else: else:
self._width = 0 self._width = 0
@ -117,7 +128,10 @@ class ImageReaderUI(QObject):
def onDepthChanged(self, value): def onDepthChanged(self, value):
if self._ui_view and not self._disable_size_callbacks: if self._ui_view and not self._disable_size_callbacks:
if len(value) > 0: if len(value) > 0:
try:
self._depth = float(value.replace(",", ".")) self._depth = float(value.replace(",", "."))
except ValueError: # Can happen with incomplete numbers, such as "-".
self._depth = 0
else: else:
self._depth = 0 self._depth = 0
@ -128,15 +142,21 @@ class ImageReaderUI(QObject):
@pyqtSlot(str) @pyqtSlot(str)
def onBaseHeightChanged(self, value): def onBaseHeightChanged(self, value):
if (len(value) > 0): if len(value) > 0:
try:
self.base_height = float(value.replace(",", ".")) self.base_height = float(value.replace(",", "."))
except ValueError: # Can happen with incomplete numbers, such as "-".
self.base_height = 0
else: else:
self.base_height = 0 self.base_height = 0
@pyqtSlot(str) @pyqtSlot(str)
def onPeakHeightChanged(self, value): def onPeakHeightChanged(self, value):
if (len(value) > 0): if len(value) > 0:
try:
self.peak_height = float(value.replace(",", ".")) self.peak_height = float(value.replace(",", "."))
except ValueError: # Can happen with incomplete numbers, such as "-".
self._width = 0
else: else:
self.peak_height = 0 self.peak_height = 0

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Enables ability to generate printable geometry from 2D image files.", "description": "Enables ability to generate printable geometry from 2D image files.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides support for importing profiles from legacy Cura versions.", "description": "Provides support for importing profiles from legacy Cura versions.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View File

@ -107,7 +107,7 @@ Item
labelWidth: base.labelWidth labelWidth: base.labelWidth
controlWidth: base.controlWidth controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm") unitText: catalog.i18nc("@label", "mm")
allowNegativeValue: true minimum: Number.NEGATIVE_INFINITY
forceUpdateOnChangeFunction: forceUpdateFunction forceUpdateOnChangeFunction: forceUpdateFunction
} }
@ -122,7 +122,7 @@ Item
labelWidth: base.labelWidth labelWidth: base.labelWidth
controlWidth: base.controlWidth controlWidth: base.controlWidth
unitText: catalog.i18nc("@label", "mm") unitText: catalog.i18nc("@label", "mm")
allowNegativeValue: true minimum: Number.NEGATIVE_INFINITY
forceUpdateOnChangeFunction: forceUpdateFunction forceUpdateOnChangeFunction: forceUpdateFunction
} }

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