Merge branch 'master' into infill_mesh_shell_defaults

This commit is contained in:
Ghostkeeper 2020-02-11 17:21:19 +01:00
commit 53e1742d27
No known key found for this signature in database
GPG key ID: 37E2020986774393
341 changed files with 77506 additions and 69425 deletions

View file

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

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

@ -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

@ -28,11 +28,12 @@ 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:
if application is None: raise RuntimeError("Tried to create singleton '{class_name}' more than once.".format(class_name = CuraAPI.__name__))
raise Exception("Upon first time creation, the application must be set.") if application is None:
cls.__instance = super(CuraAPI, cls).__new__(cls) raise RuntimeError("Upon first time creation, the application must be set.")
cls._application = application cls.__instance = super(CuraAPI, cls).__new__(cls)
cls._application = application
return cls.__instance return cls.__instance
def __init__(self, application: Optional["CuraApplication"] = None) -> None: def __init__(self, application: Optional["CuraApplication"] = None) -> None:

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) 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 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

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

@ -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

@ -12,9 +12,13 @@ import json
import locale import locale
from typing import cast from typing import cast
from sentry_sdk.hub import Hub try:
from sentry_sdk.utils import event_from_exception from sentry_sdk.hub import Hub
from sentry_sdk import configure_scope from sentry_sdk.utils import event_from_exception
from sentry_sdk import configure_scope
with_sentry_sdk = True
except ImportError:
with_sentry_sdk = False
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
@ -54,6 +58,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):
@ -66,8 +72,9 @@ class CrashHandler:
if has_started and exception_type in skip_exception_types: if has_started and exception_type in skip_exception_types:
return return
with configure_scope() as scope: if with_sentry_sdk:
scope.set_tag("during_startup", not has_started) with configure_scope() as scope:
scope.set_tag("during_startup", not has_started)
if not has_started: if not has_started:
self._send_report_checkbox = None self._send_report_checkbox = None
@ -203,16 +210,17 @@ class CrashHandler:
layout.addWidget(label) layout.addWidget(label)
group.setLayout(layout) group.setLayout(layout)
with configure_scope() as scope: if with_sentry_sdk:
scope.set_tag("qt_version", QT_VERSION_STR) with configure_scope() as scope:
scope.set_tag("pyqt_version", PYQT_VERSION_STR) scope.set_tag("qt_version", QT_VERSION_STR)
scope.set_tag("os", platform.system()) scope.set_tag("pyqt_version", PYQT_VERSION_STR)
scope.set_tag("os_version", platform.version()) scope.set_tag("os", platform.system())
scope.set_tag("locale_os", self.data["locale_os"]) scope.set_tag("os_version", platform.version())
scope.set_tag("locale_cura", self.cura_locale) scope.set_tag("locale_os", self.data["locale_os"])
scope.set_tag("is_enterprise", ApplicationMetadata.IsEnterpriseVersion) scope.set_tag("locale_cura", self.cura_locale)
scope.set_tag("is_enterprise", ApplicationMetadata.IsEnterpriseVersion)
scope.set_user({"id": str(uuid.getnode())})
scope.set_user({"id": str(uuid.getnode())})
return group return group
@ -247,12 +255,13 @@ class CrashHandler:
except: except:
pass pass
with configure_scope() as scope: if with_sentry_sdk:
scope.set_tag("opengl_version", opengl_instance.getOpenGLVersion()) with configure_scope() as scope:
scope.set_tag("gpu_vendor", opengl_instance.getGPUVendorName()) scope.set_tag("opengl_version", opengl_instance.getOpenGLVersion())
scope.set_tag("gpu_type", opengl_instance.getGPUType()) scope.set_tag("gpu_vendor", opengl_instance.getGPUVendorName())
scope.set_tag("active_machine", active_machine_definition_id) scope.set_tag("gpu_type", opengl_instance.getGPUType())
scope.set_tag("active_machine_manufacturer", active_machine_manufacturer) scope.set_tag("active_machine", active_machine_definition_id)
scope.set_tag("active_machine_manufacturer", active_machine_manufacturer)
return info return info
@ -335,9 +344,10 @@ class CrashHandler:
"module_name": module_name, "version": module_version, "is_plugin": isPlugin} "module_name": module_name, "version": module_version, "is_plugin": isPlugin}
self.data["exception"] = exception_dict self.data["exception"] = exception_dict
with configure_scope() as scope: if with_sentry_sdk:
scope.set_tag("is_plugin", isPlugin) with configure_scope() as scope:
scope.set_tag("module", module_name) scope.set_tag("is_plugin", isPlugin)
scope.set_tag("module", module_name)
return group return group
@ -396,15 +406,24 @@ class CrashHandler:
# Before sending data, the user comments are stored # Before sending data, the user comments are stored
self.data["user_info"] = self.user_description_text_area.toPlainText() self.data["user_info"] = self.user_description_text_area.toPlainText()
try: if with_sentry_sdk:
hub = Hub.current try:
event, hint = event_from_exception((self.exception_type, self.value, self.traceback)) hub = Hub.current
hub.capture_event(event, hint=hint) event, hint = event_from_exception((self.exception_type, self.value, self.traceback))
hub.flush() hub.capture_event(event, hint=hint)
except Exception as e: # We don't want any exception to cause problems hub.flush()
Logger.logException("e", "An exception occurred while trying to send crash report") except Exception as e: # We don't want any exception to cause problems
Logger.logException("e", "An exception occurred while trying to send crash report")
if not self.has_started:
print("An exception occurred while trying to send crash report: %s" % e)
else:
msg = "SentrySDK is not available and the report could not be sent."
Logger.logException("e", msg)
if not self.has_started: if not self.has_started:
print("An exception occurred while trying to send crash report: %s" % e) print(msg)
print("Exception type: {}".format(self.exception_type))
print("Value: {}".format(self.value))
print("Traceback: {}".format(self.traceback))
os._exit(1) os._exit(1)

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,10 +1,10 @@
# 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 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
import numpy import numpy
@ -15,7 +15,7 @@ from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qm
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
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.Message import Message
@ -130,6 +130,8 @@ from . import CameraAnimation
from . import CuraActions from . import CuraActions
from . import PrintJobPreviewImageProvider from . import PrintJobPreviewImageProvider
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
from cura import ApplicationMetadata, UltimakerCloudAuthentication from cura import ApplicationMetadata, UltimakerCloudAuthentication
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
@ -189,9 +191,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
@ -264,7 +264,6 @@ class CuraApplication(QtApplication):
# Backups # Backups
self._auto_save = None # type: Optional[AutoSave] self._auto_save = None # type: Optional[AutoSave]
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
@ -349,6 +348,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")
@ -392,6 +394,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:
@ -631,6 +635,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
@ -697,7 +707,7 @@ class CuraApplication(QtApplication):
self._message_box_callback_arguments = [] self._message_box_callback_arguments = []
# 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:
# 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
@ -760,7 +770,7 @@ class CuraApplication(QtApplication):
Logger.log("i", "Initializing machine manager") Logger.log("i", "Initializing machine manager")
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing machine manager...")) self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing machine manager..."))
self._machine_manager = MachineManager(self, parent = self) self.getMachineManager()
self.processEvents() self.processEvents()
Logger.log("i", "Initializing container manager") Logger.log("i", "Initializing container manager")
@ -933,7 +943,7 @@ class CuraApplication(QtApplication):
def getMachineManager(self, *args) -> MachineManager: def getMachineManager(self, *args) -> MachineManager:
if self._machine_manager is None: if self._machine_manager is None:
self._machine_manager = MachineManager(self) self._machine_manager = MachineManager(self, parent = self)
return self._machine_manager return self._machine_manager
def getExtruderManager(self, *args) -> ExtruderManager: def getExtruderManager(self, *args) -> ExtruderManager:
@ -987,8 +997,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:
@ -1441,7 +1451,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)
@ -1491,7 +1501,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)
@ -1673,7 +1683,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(
@ -1794,8 +1804,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)
@ -1826,15 +1836,21 @@ 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"))
parent = node.getParent() if not selection_pass: # If you right-click before the rendering has been initialised there might not be a selection pass yet.
while(parent and parent.callDecoration("isGroup")): print("--------------ding! Got the crash.")
node = parent return
parent = node.getParent() node = self.getController().getScene().findObject(selection_pass.getIdAtPosition(x, y))
if not node:
return
parent = node.getParent()
while parent and parent.callDecoration("isGroup"):
node = parent
parent = node.getParent()
Selection.add(node) Selection.add(node)
@pyqtSlot() @pyqtSlot()
def showMoreInformationDialogForAnonymousDataCollection(self): def showMoreInformationDialogForAnonymousDataCollection(self):
@ -1869,16 +1885,14 @@ 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)
def appHeight(self) -> int: def appHeight(self) -> int:
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()
def deleteAll(self, only_selectable: bool = True) -> None: def deleteAll(self, only_selectable: bool = True) -> None:

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,8 +16,7 @@ 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):
return self._layers return self._layers

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

@ -45,7 +45,7 @@ class BaseMaterialsModel(ListModel):
# can be caused in the middle of a XMLMaterial loading, and the material container we try to find may not be # can be caused in the middle of a XMLMaterial loading, and the material container we try to find may not be
# in the system yet. This will cause an infinite recursion of (1) trying to load a material, (2) trying to # in the system yet. This will cause an infinite recursion of (1) trying to load a material, (2) trying to
# update the material model, (3) cannot find the material container, load it, (4) repeat #1. # update the material model, (3) cannot find the material container, load it, (4) repeat #1.
self._update_timer = QTimer() self._update_timer = QTimer(self)
self._update_timer.setInterval(100) self._update_timer.setInterval(100)
self._update_timer.setSingleShot(True) self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._update) self._update_timer.timeout.connect(self._update)

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

@ -41,4 +41,4 @@ class QualityNode(ContainerNode):
self.intents[intent["id"]] = IntentNode(intent["id"], quality = self) self.intents[intent["id"]] = IntentNode(intent["id"], quality = self)
self.intents["empty_intent"] = IntentNode("empty_intent", quality = self) self.intents["empty_intent"] = IntentNode("empty_intent", quality = self)
# Otherwise, there are no intents for global profiles. # Otherwise, there are no intents for global profiles.

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

@ -36,7 +36,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.
@ -53,6 +54,7 @@ class LocalAuthorizationServer:
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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -227,7 +227,7 @@ class MachineManager(QObject):
except TypeError: except TypeError:
pass pass
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks(): for extruder_stack in self._global_container_stack.extruderList:
extruder_stack.propertyChanged.disconnect(self._onPropertyChanged) extruder_stack.propertyChanged.disconnect(self._onPropertyChanged)
extruder_stack.containersChanged.disconnect(self._onContainersChanged) extruder_stack.containersChanged.disconnect(self._onContainersChanged)
@ -257,7 +257,7 @@ class MachineManager(QObject):
self._global_container_stack.setMaterial(empty_material_container) self._global_container_stack.setMaterial(empty_material_container)
# Listen for changes on all extruder stacks # Listen for changes on all extruder stacks
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks(): for extruder_stack in self._global_container_stack.extruderList:
extruder_stack.propertyChanged.connect(self._onPropertyChanged) extruder_stack.propertyChanged.connect(self._onPropertyChanged)
extruder_stack.containersChanged.connect(self._onContainersChanged) extruder_stack.containersChanged.connect(self._onContainersChanged)
@ -365,7 +365,7 @@ class MachineManager(QObject):
# Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are # Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() extruder_stacks = self._global_container_stack.extruderList
count = 1 # We start with the global stack count = 1 # We start with the global stack
for stack in extruder_stacks: for stack in extruder_stacks:
md = stack.getMetaData() md = stack.getMetaData()
@ -388,8 +388,7 @@ class MachineManager(QObject):
if self._global_container_stack.getTop().getNumInstances() != 0: if self._global_container_stack.getTop().getNumInstances() != 0:
return True return True
stacks = ExtruderManager.getInstance().getActiveExtruderStacks() for stack in self._global_container_stack.extruderList:
for stack in stacks:
if stack.getTop().getNumInstances() != 0: if stack.getTop().getNumInstances() != 0:
return True return True
@ -399,8 +398,7 @@ class MachineManager(QObject):
def numUserSettings(self) -> int: def numUserSettings(self) -> int:
if not self._global_container_stack: if not self._global_container_stack:
return 0 return 0
num_user_settings = 0 num_user_settings = self._global_container_stack.getTop().getNumInstances()
num_user_settings += self._global_container_stack.getTop().getNumInstances()
stacks = self._global_container_stack.extruderList stacks = self._global_container_stack.extruderList
for stack in stacks: for stack in stacks:
num_user_settings += stack.getTop().getNumInstances() num_user_settings += stack.getTop().getNumInstances()
@ -427,7 +425,7 @@ class MachineManager(QObject):
stack = ExtruderManager.getInstance().getActiveExtruderStack() stack = ExtruderManager.getInstance().getActiveExtruderStack()
stacks = [stack] stacks = [stack]
else: else:
stacks = ExtruderManager.getInstance().getActiveExtruderStacks() stacks = self._global_container_stack.extruderList
for stack in stacks: for stack in stacks:
if stack is not None: if stack is not None:
@ -612,10 +610,9 @@ class MachineManager(QObject):
if self._active_container_stack is None or self._global_container_stack is None: if self._active_container_stack is None or self._global_container_stack is None:
return return
new_value = self._active_container_stack.getProperty(key, "value") new_value = self._active_container_stack.getProperty(key, "value")
extruder_stacks = [stack for stack in ExtruderManager.getInstance().getActiveExtruderStacks()]
# Check in which stack the value has to be replaced # Check in which stack the value has to be replaced
for extruder_stack in extruder_stacks: for extruder_stack in self._global_container_stack.extruderList:
if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value: if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved
@ -750,6 +747,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
@ -798,7 +800,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
@ -929,7 +931,7 @@ class MachineManager(QObject):
def _getContainerChangedSignals(self) -> List[Signal]: def _getContainerChangedSignals(self) -> List[Signal]:
if self._global_container_stack is None: if self._global_container_stack is None:
return [] return []
return [s.containersChanged for s in ExtruderManager.getInstance().getActiveExtruderStacks() + [self._global_container_stack]] return [s.containersChanged for s in self._global_container_stack.extruderList + [self._global_container_stack]]
@pyqtSlot(str, str, str) @pyqtSlot(str, str, str)
def setSettingForAllExtruders(self, setting_name: str, property_name: str, property_value: str) -> None: def setSettingForAllExtruders(self, setting_name: str, property_name: str, property_value: str) -> None:

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

@ -12,7 +12,11 @@ from UM.Platform import Platform
from cura import ApplicationMetadata from cura import ApplicationMetadata
from cura.ApplicationMetadata import CuraAppName from cura.ApplicationMetadata import CuraAppName
import sentry_sdk try:
import sentry_sdk
with_sentry_sdk = True
except ImportError:
with_sentry_sdk = False
parser = argparse.ArgumentParser(prog = "cura", parser = argparse.ArgumentParser(prog = "cura",
add_help = False) add_help = False)
@ -24,21 +28,26 @@ parser.add_argument("--debug",
known_args = vars(parser.parse_known_args()[0]) known_args = vars(parser.parse_known_args()[0])
sentry_env = "production" if with_sentry_sdk:
if ApplicationMetadata.CuraVersion == "master": sentry_env = "unknown" # Start off with a "IDK"
sentry_env = "development" if hasattr(sys, "frozen"):
try: sentry_env = "production" # A frozen build is a "real" distribution.
if ApplicationMetadata.CuraVersion.split(".")[2] == "99": elif ApplicationMetadata.CuraVersion == "master":
sentry_env = "nightly" sentry_env = "development"
except IndexError: elif "beta" in ApplicationMetadata.CuraVersion or "BETA" in ApplicationMetadata.CuraVersion:
pass sentry_env = "beta"
try:
sentry_sdk.init("https://5034bf0054fb4b889f82896326e79b13@sentry.io/1821564", if ApplicationMetadata.CuraVersion.split(".")[2] == "99":
environment = sentry_env, sentry_env = "nightly"
release = "cura%s" % ApplicationMetadata.CuraVersion, except IndexError:
default_integrations = False, pass
max_breadcrumbs = 300,
server_name = "cura") sentry_sdk.init("https://5034bf0054fb4b889f82896326e79b13@sentry.io/1821564",
environment = sentry_env,
release = "cura%s" % ApplicationMetadata.CuraVersion,
default_integrations = False,
max_breadcrumbs = 300,
server_name = "cura")
if not known_args["debug"]: if not known_args["debug"]:
def get_cura_dir_path(): def get_cura_dir_path():
@ -162,6 +171,7 @@ else:
# tries to create PyQt objects on a non-main thread. # tries to create PyQt objects on a non-main thread.
import Arcus #@UnusedImport import Arcus #@UnusedImport
import Savitar #@UnusedImport import Savitar #@UnusedImport
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication

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
fi if [ -z "${git_ref_name}" ]; then
GIT_REF_NAME="$(basename "${GIT_REF_NAME}")" continue
fi
URANIUM_BRANCH="${GIT_REF_NAME:-master}" git_ref_name="$(basename "${git_ref_name}")"
output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")" # Skip refs/pull/1234/merge as pull requests use it as GITHUB_REF
if [ -z "${output}" ]; then if [[ "${git_ref_name}" == "merge" ]]; then
echo "Could not find Uranium banch ${URANIUM_BRANCH}, fallback to use master." echo "Skip [${git_ref_name}]"
URANIUM_BRANCH="master" continue
fi fi
URANIUM_BRANCH="${git_ref_name}"
output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")"
if [ -n "${output}" ]; then
echo "Found Uranium branch [${URANIUM_BRANCH}]."
break
else
echo "Could not find Uranium banch [${URANIUM_BRANCH}], try next."
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

@ -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
@ -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:
@ -1005,8 +1024,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# Set metadata fields that are missing from the global stack # Set metadata fields that are missing from the global stack
for key, value in self._machine_info.metadata_dict.items(): for key, value in self._machine_info.metadata_dict.items():
if key not in global_stack.getMetaData(): global_stack.setMetaDataEntry(key, value)
global_stack.setMetaDataEntry(key, value)
def _updateActiveMachine(self, global_stack): def _updateActiveMachine(self, global_stack):
# Actually change the active machine. # Actually change the active machine.

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

@ -73,11 +73,25 @@ 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()
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):

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

@ -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

@ -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.")

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
@ -153,7 +153,7 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.MaterialIncompatible) self.setResult(StartJobResult.MaterialIncompatible)
return return
for position, extruder_stack in stack.extruders.items(): for extruder_stack in stack.extruderList:
material = extruder_stack.findContainer({"type": "material"}) material = extruder_stack.findContainer({"type": "material"})
if not extruder_stack.isEnabled: if not extruder_stack.isEnabled:
continue continue
@ -162,7 +162,6 @@ class StartSliceJob(Job):
self.setResult(StartJobResult.MaterialIncompatible) self.setResult(StartJobResult.MaterialIncompatible)
return return
# Don't slice if there is a per object setting with an error value. # Don't slice if there is a per object setting with an error value.
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()):
if not isinstance(node, CuraSceneNode) or not node.isSelectable(): if not isinstance(node, CuraSceneNode) or not node.isSelectable():
@ -172,146 +171,145 @@ 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: # Singe we walk through all nodes in the scene, they always have a parent.
# Singe we walk through all nodes in the scene, they always have a parent. cast(SceneNode, node.getParent()).removeChild(node)
cast(SceneNode, node.getParent()).removeChild(node) break
break
# Get the objects in their groups to print. # Get the objects in their groups to print.
object_groups = [] object_groups = []
if stack.getProperty("print_sequence", "value") == "one_at_a_time": if stack.getProperty("print_sequence", "value") == "one_at_a_time":
for node in OneAtATimeIterator(self._scene.getRoot()): for node in OneAtATimeIterator(self._scene.getRoot()):
temp_list = []
# Node can't be printed, so don't bother sending it.
if getattr(node, "_outside_buildarea", False):
continue
# Filter on current build plate
build_plate_number = node.callDecoration("getBuildPlateNumber")
if build_plate_number is not None and build_plate_number != self._build_plate_number:
continue
children = node.getAllChildren()
children.append(node)
for child_node in children:
mesh_data = child_node.getMeshData()
if mesh_data and mesh_data.getVertices() is not None:
temp_list.append(child_node)
if temp_list:
object_groups.append(temp_list)
Job.yieldThread()
if len(object_groups) == 0:
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
else:
temp_list = [] temp_list = []
has_printing_mesh = False
for node in DepthFirstIterator(self._scene.getRoot()):
mesh_data = node.getMeshData()
if node.callDecoration("isSliceable") and mesh_data and mesh_data.getVertices() is not None:
is_non_printing_mesh = bool(node.callDecoration("isNonPrintingMesh"))
# Find a reason not to add the node # Node can't be printed, so don't bother sending it.
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number: if getattr(node, "_outside_buildarea", False):
continue continue
if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh:
continue
temp_list.append(node) # Filter on current build plate
if not is_non_printing_mesh: build_plate_number = node.callDecoration("getBuildPlateNumber")
has_printing_mesh = True if build_plate_number is not None and build_plate_number != self._build_plate_number:
continue
Job.yieldThread() children = node.getAllChildren()
children.append(node)
# If the list doesn't have any model with suitable settings then clean the list for child_node in children:
# otherwise CuraEngine will crash mesh_data = child_node.getMeshData()
if not has_printing_mesh: if mesh_data and mesh_data.getVertices() is not None:
temp_list.clear() temp_list.append(child_node)
if temp_list: if temp_list:
object_groups.append(temp_list) object_groups.append(temp_list)
Job.yieldThread()
if len(object_groups) == 0:
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
else:
temp_list = []
has_printing_mesh = False
for node in DepthFirstIterator(self._scene.getRoot()):
mesh_data = node.getMeshData()
if node.callDecoration("isSliceable") and mesh_data and mesh_data.getVertices() is not None:
is_non_printing_mesh = bool(node.callDecoration("isNonPrintingMesh"))
global_stack = CuraApplication.getInstance().getGlobalContainerStack() # Find a reason not to add the node
if not global_stack: if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:
return continue
extruders_enabled = {position: stack.isEnabled for position, stack in global_stack.extruders.items()} if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh:
filtered_object_groups = []
has_model_with_disabled_extruders = False
associated_disabled_extruders = set()
for group in object_groups:
stack = global_stack
skip_group = False
for node in group:
# Only check if the printing extruder is enabled for printing meshes
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
extruder_position = node.callDecoration("getActiveExtruderPosition")
if not is_non_printing_mesh and not extruders_enabled[extruder_position]:
skip_group = True
has_model_with_disabled_extruders = True
associated_disabled_extruders.add(extruder_position)
if not skip_group:
filtered_object_groups.append(group)
if has_model_with_disabled_extruders:
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
associated_disabled_extruders = {str(c) for c in sorted([int(p) + 1 for p in associated_disabled_extruders])}
self.setMessage(", ".join(associated_disabled_extruders))
return
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
# the build volume)
if not filtered_object_groups:
self.setResult(StartJobResult.NothingToSlice)
return
self._buildGlobalSettingsMessage(stack)
self._buildGlobalInheritsStackMessage(stack)
# Build messages for extruder stacks
for extruder_stack in global_stack.extruderList:
self._buildExtruderMessage(extruder_stack)
for group in filtered_object_groups:
group_message = self._slice_message.addRepeatedMessage("object_lists")
parent = group[0].getParent()
if parent is not None and parent.callDecoration("isGroup"):
self._handlePerObjectSettings(cast(CuraSceneNode, parent), group_message)
for object in group:
mesh_data = object.getMeshData()
if mesh_data is None:
continue continue
rot_scale = object.getWorldTransformation().getTransposed().getData()[0:3, 0:3]
translate = object.getWorldTransformation().getData()[:3, 3]
# This effectively performs a limited form of MeshData.getTransformed that ignores normals. temp_list.append(node)
verts = mesh_data.getVertices() if not is_non_printing_mesh:
verts = verts.dot(rot_scale) has_printing_mesh = True
verts += translate
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation. Job.yieldThread()
verts[:, [1, 2]] = verts[:, [2, 1]]
verts[:, 1] *= -1
obj = group_message.addRepeatedMessage("objects") # If the list doesn't have any model with suitable settings then clean the list
obj.id = id(object) # otherwise CuraEngine will crash
obj.name = object.getName() if not has_printing_mesh:
indices = mesh_data.getIndices() temp_list.clear()
if indices is not None:
flat_verts = numpy.take(verts, indices.flatten(), axis=0)
else:
flat_verts = numpy.array(verts)
obj.vertices = flat_verts if temp_list:
object_groups.append(temp_list)
self._handlePerObjectSettings(cast(CuraSceneNode, object), obj) global_stack = CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack:
return
extruders_enabled = {position: stack.isEnabled for position, stack in global_stack.extruders.items()}
filtered_object_groups = []
has_model_with_disabled_extruders = False
associated_disabled_extruders = set()
for group in object_groups:
stack = global_stack
skip_group = False
for node in group:
# Only check if the printing extruder is enabled for printing meshes
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
extruder_position = node.callDecoration("getActiveExtruderPosition")
if not is_non_printing_mesh and not extruders_enabled[extruder_position]:
skip_group = True
has_model_with_disabled_extruders = True
associated_disabled_extruders.add(extruder_position)
if not skip_group:
filtered_object_groups.append(group)
Job.yieldThread() if has_model_with_disabled_extruders:
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
associated_disabled_extruders = {str(c) for c in sorted([int(p) + 1 for p in associated_disabled_extruders])}
self.setMessage(", ".join(associated_disabled_extruders))
return
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
# the build volume)
if not filtered_object_groups:
self.setResult(StartJobResult.NothingToSlice)
return
self._buildGlobalSettingsMessage(stack)
self._buildGlobalInheritsStackMessage(stack)
# Build messages for extruder stacks
for extruder_stack in global_stack.extruderList:
self._buildExtruderMessage(extruder_stack)
for group in filtered_object_groups:
group_message = self._slice_message.addRepeatedMessage("object_lists")
parent = group[0].getParent()
if parent is not None and parent.callDecoration("isGroup"):
self._handlePerObjectSettings(cast(CuraSceneNode, parent), group_message)
for object in group:
mesh_data = object.getMeshData()
if mesh_data is None:
continue
rot_scale = object.getWorldTransformation().getTransposed().getData()[0:3, 0:3]
translate = object.getWorldTransformation().getData()[:3, 3]
# This effectively performs a limited form of MeshData.getTransformed that ignores normals.
verts = mesh_data.getVertices()
verts = verts.dot(rot_scale)
verts += translate
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation.
verts[:, [1, 2]] = verts[:, [2, 1]]
verts[:, 1] *= -1
obj = group_message.addRepeatedMessage("objects")
obj.id = id(object)
obj.name = object.getName()
indices = mesh_data.getIndices()
if indices is not None:
flat_verts = numpy.take(verts, indices.flatten(), axis=0)
else:
flat_verts = numpy.array(verts)
obj.vertices = flat_verts
self._handlePerObjectSettings(cast(CuraSceneNode, object), obj)
Job.yieldThread()
self.setResult(StartJobResult.Finished) self.setResult(StartJobResult.Finished)
@ -345,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

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

@ -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
@ -258,16 +258,19 @@ class FlavorParser:
continue continue
if item.startswith(";"): if item.startswith(";"):
continue continue
if item[0] == "X": try:
x = float(item[1:]) if item[0] == "X":
if item[0] == "Y": x = float(item[1:])
y = float(item[1:]) if item[0] == "Y":
if item[0] == "Z": y = float(item[1:])
z = float(item[1:]) if item[0] == "Z":
if item[0] == "F": z = float(item[1:])
f = float(item[1:]) / 60 if item[0] == "F":
if item[0] == "E": f = float(item[1:]) / 60
e = float(item[1:]) if item[0] == "E":
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

@ -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

@ -1,8 +1,8 @@
{ {
"name": "Machine Settings action", "name": "Machine Settings Action",
"author": "fieldOfView", "author": "fieldOfView, Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).", "description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -2,7 +2,7 @@
"name": "Model Checker", "name": "Model Checker",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"api": "7.0", "api": "7.1",
"description": "Checks models and print configuration for possible printing issues and give suggestions.", "description": "Checks models and print configuration for possible printing issues and give suggestions.",
"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 monitor stage in Cura.", "description": "Provides a monitor stage in Cura.",
"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 the Per Model Settings.", "description": "Provides the Per Model Settings.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -44,6 +44,9 @@ class PostProcessingPlugin(QObject, Extension):
# There can be duplicates, which will be executed in sequence. # There can be duplicates, which will be executed in sequence.
self._script_list = [] # type: List[Script] self._script_list = [] # type: List[Script]
self._selected_script_index = -1 self._selected_script_index = -1
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
if self._global_container_stack:
self._global_container_stack.metaDataChanged.connect(self._restoreScriptInforFromMetadata)
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute) Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) # When the current printer changes, update the list of scripts. Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) # When the current printer changes, update the list of scripts.
@ -209,33 +212,34 @@ class PostProcessingPlugin(QObject, Extension):
self.scriptListChanged.emit() self.scriptListChanged.emit()
self._propertyChanged() self._propertyChanged()
## When the global container stack is changed, swap out the list of active def _restoreScriptInforFromMetadata(self):
# scripts.
def _onGlobalContainerStackChanged(self) -> None:
self.loadAllScripts() self.loadAllScripts()
new_stack = Application.getInstance().getGlobalContainerStack() new_stack = self._global_container_stack
if new_stack is None: if new_stack is None:
return return
self._script_list.clear() self._script_list.clear()
if not new_stack.getMetaDataEntry("post_processing_scripts"): # Missing or empty. if not new_stack.getMetaDataEntry("post_processing_scripts"): # Missing or empty.
self.scriptListChanged.emit() # Even emit this if it didn't change. We want it to write the empty list to the stack's metadata. self.scriptListChanged.emit() # Even emit this if it didn't change. We want it to write the empty list to the stack's metadata.
self.setSelectedScriptIndex(-1) self.setSelectedScriptIndex(-1)
return return
self._script_list.clear() self._script_list.clear()
scripts_list_strs = new_stack.getMetaDataEntry("post_processing_scripts") scripts_list_strs = new_stack.getMetaDataEntry("post_processing_scripts")
for script_str in scripts_list_strs.split("\n"): # Encoded config files should never contain three newlines in a row. At most 2, just before section headers. for script_str in scripts_list_strs.split(
"\n"): # Encoded config files should never contain three newlines in a row. At most 2, just before section headers.
if not script_str: # There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here). if not script_str: # There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here).
continue continue
script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") # Unescape escape sequences. script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") # Unescape escape sequences.
script_parser = configparser.ConfigParser(interpolation = None) script_parser = configparser.ConfigParser(interpolation=None)
script_parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive. script_parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive.
script_parser.read_string(script_str) script_parser.read_string(script_str)
for script_name, settings in script_parser.items(): # There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script. for script_name, settings in script_parser.items(): # There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script.
if script_name == "DEFAULT": # ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one. if script_name == "DEFAULT": # ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one.
continue continue
if script_name not in self._loaded_scripts: # Don't know this post-processing plug-in. if script_name not in self._loaded_scripts: # Don't know this post-processing plug-in.
Logger.log("e", "Unknown post-processing script {script_name} was encountered in this global stack.".format(script_name = script_name)) Logger.log("e",
"Unknown post-processing script {script_name} was encountered in this global stack.".format(
script_name=script_name))
continue continue
new_script = self._loaded_scripts[script_name]() new_script = self._loaded_scripts[script_name]()
new_script.initialize() new_script.initialize()
@ -245,7 +249,22 @@ class PostProcessingPlugin(QObject, Extension):
self._script_list.append(new_script) self._script_list.append(new_script)
self.setSelectedScriptIndex(0) self.setSelectedScriptIndex(0)
# Ensure that we always force an update (otherwise the fields don't update correctly!)
self.selectedIndexChanged.emit()
self.scriptListChanged.emit() self.scriptListChanged.emit()
self._propertyChanged()
## When the global container stack is changed, swap out the list of active
# scripts.
def _onGlobalContainerStackChanged(self) -> None:
if self._global_container_stack:
self._global_container_stack.metaDataChanged.disconnect(self._restoreScriptInforFromMetadata)
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
if self._global_container_stack:
self._global_container_stack.metaDataChanged.connect(self._restoreScriptInforFromMetadata)
self._restoreScriptInforFromMetadata()
@pyqtSlot() @pyqtSlot()
def writeScriptsToStack(self) -> None: def writeScriptsToStack(self) -> None:
@ -267,14 +286,18 @@ class PostProcessingPlugin(QObject, Extension):
script_list_string = "\n".join(script_list_strs) # ConfigParser should never output three newlines in a row when serialised, so it's a safe delimiter. script_list_string = "\n".join(script_list_strs) # ConfigParser should never output three newlines in a row when serialised, so it's a safe delimiter.
global_stack = Application.getInstance().getGlobalContainerStack() if self._global_container_stack is None:
if global_stack is None:
return return
if "post_processing_scripts" not in global_stack.getMetaData(): # Ensure we don't get triggered by our own write.
global_stack.setMetaDataEntry("post_processing_scripts", "") self._global_container_stack.metaDataChanged.disconnect(self._restoreScriptInforFromMetadata)
global_stack.setMetaDataEntry("post_processing_scripts", script_list_string) if "post_processing_scripts" not in self._global_container_stack.getMetaData():
self._global_container_stack.setMetaDataEntry("post_processing_scripts", "")
self._global_container_stack.setMetaDataEntry("post_processing_scripts", script_list_string)
# We do want to listen to other events.
self._global_container_stack.metaDataChanged.connect(self._restoreScriptInforFromMetadata)
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
def _createView(self) -> None: def _createView(self) -> None:

View file

@ -2,7 +2,7 @@
"name": "Post Processing", "name": "Post Processing",
"author": "Ultimaker", "author": "Ultimaker",
"version": "2.2.1", "version": "2.2.1",
"api": "7.0", "api": "7.1",
"description": "Extension that allows for user created scripts for post processing", "description": "Extension that allows for user created scripts for post processing",
"catalog": "cura" "catalog": "cura"
} }

View file

@ -72,18 +72,25 @@ class DisplayFilenameAndLayerOnLCD(Script):
lcd_text = "M117 Printing " + name + " - Layer " lcd_text = "M117 Printing " + name + " - Layer "
i = self.getSettingValueByKey("startNum") i = self.getSettingValueByKey("startNum")
for layer in data: for layer in data:
display_text = lcd_text + str(i) + " " + name display_text = lcd_text + str(i)
layer_index = data.index(layer) layer_index = data.index(layer)
lines = layer.split("\n") lines = layer.split("\n")
for line in lines: for line in lines:
if line.startswith(";LAYER_COUNT:"): if line.startswith(";LAYER_COUNT:"):
max_layer = line max_layer = line
max_layer = max_layer.split(":")[1] max_layer = max_layer.split(":")[1]
if self.getSettingValueByKey("startNum") == 0:
max_layer = str(int(max_layer) - 1)
if line.startswith(";LAYER:"): if line.startswith(";LAYER:"):
if self.getSettingValueByKey("maxlayer"): if self.getSettingValueByKey("maxlayer"):
display_text = display_text + " of " + max_layer display_text = display_text + " of " + max_layer
if not self.getSettingValueByKey("scroll"):
display_text = display_text + " " + name
else: else:
display_text = display_text + "!" if not self.getSettingValueByKey("scroll"):
display_text = display_text + " " + name + "!"
else:
display_text = display_text + "!"
line_index = lines.index(line) line_index = lines.index(line)
lines.insert(line_index + 1, display_text) lines.insert(line_index + 1, display_text)
i += 1 i += 1

View file

@ -35,7 +35,7 @@ class GCodeStep():
Class to store the current value of each G_Code parameter Class to store the current value of each G_Code parameter
for any G-Code step for any G-Code step
""" """
def __init__(self, step, in_relative_movement: bool = False): def __init__(self, step, in_relative_movement: bool = False) -> None:
self.step = step self.step = step
self.step_x = 0 self.step_x = 0
self.step_y = 0 self.step_y = 0

View file

@ -2,6 +2,7 @@
from ..Script import Script from ..Script import Script
class TimeLapse(Script): class TimeLapse(Script):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -75,21 +76,29 @@ class TimeLapse(Script):
trigger_command = self.getSettingValueByKey("trigger_command") trigger_command = self.getSettingValueByKey("trigger_command")
pause_length = self.getSettingValueByKey("pause_length") pause_length = self.getSettingValueByKey("pause_length")
gcode_to_append = ";TimeLapse Begin\n" gcode_to_append = ";TimeLapse Begin\n"
last_x = 0
last_y = 0
if park_print_head: if park_print_head:
gcode_to_append += self.putValue(G = 1, F = feed_rate, X = x_park, Y = y_park) + " ;Park print head\n" gcode_to_append += self.putValue(G=1, F=feed_rate,
gcode_to_append += self.putValue(M = 400) + " ;Wait for moves to finish\n" X=x_park, Y=y_park) + " ;Park print head\n"
gcode_to_append += self.putValue(M=400) + " ;Wait for moves to finish\n"
gcode_to_append += trigger_command + " ;Snap Photo\n" gcode_to_append += trigger_command + " ;Snap Photo\n"
gcode_to_append += self.putValue(G = 4, P = pause_length) + " ;Wait for camera\n" gcode_to_append += self.putValue(G=4, P=pause_length) + " ;Wait for camera\n"
gcode_to_append += ";TimeLapse End\n"
for layer in data: for idx, layer in enumerate(data):
for line in layer.split("\n"):
if self.getValue(line, "G") in {0, 1}: # Track X,Y location.
last_x = self.getValue(line, "X", last_x)
last_y = self.getValue(line, "Y", last_y)
# Check that a layer is being printed # Check that a layer is being printed
lines = layer.split("\n") lines = layer.split("\n")
for line in lines: for line in lines:
if ";LAYER:" in line: if ";LAYER:" in line:
index = data.index(layer)
layer += gcode_to_append layer += gcode_to_append
data[index] = layer layer += "G0 X%s Y%s\n" % (last_x, last_y)
data[idx] = layer
break break
return data return data

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 prepare stage in Cura.", "description": "Provides a prepare stage in Cura.",
"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 preview stage in Cura.", "description": "Provides a preview stage in Cura.",
"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.",
"description": "Provides removable drive hotplugging and writing support.", "description": "Provides removable drive hotplugging and writing support.",
"version": "1.0.1", "version": "1.0.1",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -3,7 +3,10 @@
from UM.Logger import LogOutput from UM.Logger import LogOutput
from typing import Set from typing import Set
from sentry_sdk import add_breadcrumb try:
from sentry_sdk import add_breadcrumb
except ImportError:
pass
from typing import Optional from typing import Optional
import os import os

View file

@ -1,6 +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 typing import TYPE_CHECKING, Dict, Any from typing import TYPE_CHECKING, Dict, Any
try:
import sentry_sdk
has_sentry = True
except ImportError:
has_sentry = False
from . import SentryLogger from . import SentryLogger
@ -13,4 +18,6 @@ def getMetaData() -> Dict[str, Any]:
def register(app: "Application") -> Dict[str, Any]: def register(app: "Application") -> Dict[str, Any]:
if not has_sentry:
return {} # Nothing to do here!
return {"logger": SentryLogger.SentryLogger()} return {"logger": SentryLogger.SentryLogger()}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Logs certain events so that they can be used by the crash reporter", "description": "Logs certain events so that they can be used by the crash reporter",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -120,7 +120,10 @@ class SimulationPass(RenderPass):
end = self._layer_view.end_elements_index end = self._layer_view.end_elements_index
index = self._layer_view._current_path_num index = self._layer_view._current_path_num
offset = 0 offset = 0
for polygon in layer_data.getLayer(self._layer_view._current_layer_num).polygons: layer = layer_data.getLayer(self._layer_view._current_layer_num)
if layer is None:
continue
for polygon in layer.polygons:
# The size indicates all values in the two-dimension array, and the second dimension is # The size indicates all values in the two-dimension array, and the second dimension is
# always size 3 because we have 3D points. # always size 3 because we have 3D points.
if index >= polygon.data.size // 3 - offset: if index >= polygon.data.size // 3 - offset:

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 sys import sys
@ -116,8 +116,9 @@ class SimulationView(CuraView):
self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers")) self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
self._compatibility_mode = self._evaluateCompatibilityMode() self._compatibility_mode = self._evaluateCompatibilityMode()
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"), self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled."),
title = catalog.i18nc("@info:title", "Simulation View")) title = catalog.i18nc("@info:title", "Simulation View"))
self._slice_first_warning_message = Message(catalog.i18nc("@info:status", "Nothing is shown because you need to slice first."), title = catalog.i18nc("@info:title", "No layers to show"))
QtApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated) QtApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
@ -149,6 +150,7 @@ class SimulationView(CuraView):
if self._activity == activity: if self._activity == activity:
return return
self._activity = activity self._activity = activity
self._updateSliceWarningVisibility()
self.activityChanged.emit() self.activityChanged.emit()
def getSimulationPass(self) -> SimulationPass: def getSimulationPass(self) -> SimulationPass:
@ -543,11 +545,13 @@ class SimulationView(CuraView):
self._composite_pass.getLayerBindings().append("simulationview") self._composite_pass.getLayerBindings().append("simulationview")
self._old_composite_shader = self._composite_pass.getCompositeShader() self._old_composite_shader = self._composite_pass.getCompositeShader()
self._composite_pass.setCompositeShader(self._simulationview_composite_shader) self._composite_pass.setCompositeShader(self._simulationview_composite_shader)
self._updateSliceWarningVisibility()
elif event.type == Event.ViewDeactivateEvent: elif event.type == Event.ViewDeactivateEvent:
self._controller.getScene().getRoot().childrenChanged.disconnect(self._onSceneChanged) self._controller.getScene().getRoot().childrenChanged.disconnect(self._onSceneChanged)
Application.getInstance().getPreferences().preferenceChanged.disconnect(self._onPreferencesChanged) Application.getInstance().getPreferences().preferenceChanged.disconnect(self._onPreferencesChanged)
self._wireprint_warning_message.hide() self._wireprint_warning_message.hide()
self._slice_first_warning_message.hide()
Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged) Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
@ -661,6 +665,12 @@ class SimulationView(CuraView):
self._updateWithPreferences() self._updateWithPreferences()
def _updateSliceWarningVisibility(self):
if not self.getActivity():
self._slice_first_warning_message.show()
else:
self._slice_first_warning_message.hide()
class _CreateTopLayersJob(Job): class _CreateTopLayersJob(Job):
def __init__(self, scene: "Scene", layer_number: int, solid_layers: int) -> None: def __init__(self, scene: "Scene", layer_number: int, solid_layers: int) -> None:

View file

@ -112,7 +112,7 @@ Cura.ExpandableComponent
type_id: 1 type_id: 1
}) })
layerViewTypes.append({ layerViewTypes.append({
text: catalog.i18nc("@label:listbox", "Feedrate"), text: catalog.i18nc("@label:listbox", "Speed"),
type_id: 2 type_id: 2
}) })
layerViewTypes.append({ layerViewTypes.append({

View file

@ -11,7 +11,7 @@ if TYPE_CHECKING:
class SimulationViewProxy(QObject): class SimulationViewProxy(QObject):
def __init__(self, simulation_view: "SimulationView", parent=None): def __init__(self, simulation_view: "SimulationView", parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self._simulation_view = simulation_view self._simulation_view = simulation_view
self._current_layer = 0 self._current_layer = 0

View file

@ -80,7 +80,7 @@ vertex41core =
case 1: // "Line type" case 1: // "Line type"
v_color = a_color; v_color = a_color;
break; break;
case 2: // "Feedrate" case 2: // "Speed", or technically 'Feedrate'
v_color = feedrateGradientColor(a_feedrate, u_min_feedrate, u_max_feedrate); v_color = feedrateGradientColor(a_feedrate, u_min_feedrate, u_max_feedrate);
break; break;
case 3: // "Layer thickness" case 3: // "Layer thickness"

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.1", "version": "1.0.1",
"description": "Provides the Simulation view.", "description": "Provides the Simulation view.",
"api": "7.0", "api": "7.1",
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -72,6 +72,7 @@ Window
right: parent.right right: parent.right
} }
text: catalog.i18nc("@text:window", "Ultimaker Cura collects anonymous data in order to improve the print quality and user experience. Below is an example of all the data that is shared:") text: catalog.i18nc("@text:window", "Ultimaker Cura collects anonymous data in order to improve the print quality and user experience. Below is an example of all the data that is shared:")
color: UM.Theme.getColor("text")
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
renderType: Text.NativeRendering renderType: Text.NativeRendering
} }
@ -88,7 +89,7 @@ Window
right: parent.right right: parent.right
} }
textArea.text: manager.getExampleData() textArea.text: (manager === null) ? "" : manager.getExampleData()
textArea.textFormat: Text.RichText textArea.textFormat: Text.RichText
textArea.wrapMode: Text.Wrap textArea.wrapMode: Text.Wrap
textArea.readOnly: true textArea.readOnly: true

View file

@ -5,14 +5,13 @@ import json
import os import os
import platform import platform
import time import time
from typing import cast, Optional, Set from typing import cast, Optional, Set, TYPE_CHECKING
from PyQt5.QtCore import pyqtSlot, QObject from PyQt5.QtCore import pyqtSlot, QObject
from PyQt5.QtNetwork import QNetworkRequest
from UM.Extension import Extension from UM.Extension import Extension
from UM.Application import Application
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Message import Message
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
@ -20,7 +19,8 @@ from UM.Qt.Duration import DurationFormat
from cura import ApplicationMetadata from cura import ApplicationMetadata
from .SliceInfoJob import SliceInfoJob if TYPE_CHECKING:
from PyQt5.QtNetwork import QNetworkReply
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -36,7 +36,8 @@ class SliceInfo(QObject, Extension):
QObject.__init__(self, parent) QObject.__init__(self, parent)
Extension.__init__(self) Extension.__init__(self)
self._application = Application.getInstance() from cura.CuraApplication import CuraApplication
self._application = CuraApplication.getInstance()
self._application.getOutputDeviceManager().writeStarted.connect(self._onWriteStarted) self._application.getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
self._application.getPreferences().addPreference("info/send_slice_info", True) self._application.getPreferences().addPreference("info/send_slice_info", True)
@ -56,7 +57,7 @@ class SliceInfo(QObject, Extension):
## Perform action based on user input. ## Perform action based on user input.
# Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it. # Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it.
def messageActionTriggered(self, message_id, action_id): def messageActionTriggered(self, message_id, action_id):
Application.getInstance().getPreferences().setValue("info/asked_send_slice_info", True) self._application.getPreferences().setValue("info/asked_send_slice_info", True)
if action_id == "MoreInfo": if action_id == "MoreInfo":
self.showMoreInfoDialog() self.showMoreInfoDialog()
self.send_slice_info_message.hide() self.send_slice_info_message.hide()
@ -69,7 +70,7 @@ class SliceInfo(QObject, Extension):
def _createDialog(self, qml_name): def _createDialog(self, qml_name):
Logger.log("d", "Creating dialog [%s]", qml_name) Logger.log("d", "Creating dialog [%s]", qml_name)
file_path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name) file_path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name)
dialog = Application.getInstance().createQmlComponent(file_path, {"manager": self}) dialog = self._application.createQmlComponent(file_path, {"manager": self})
return dialog return dialog
@pyqtSlot(result = str) @pyqtSlot(result = str)
@ -87,12 +88,10 @@ class SliceInfo(QObject, Extension):
@pyqtSlot(bool) @pyqtSlot(bool)
def setSendSliceInfo(self, enabled: bool): def setSendSliceInfo(self, enabled: bool):
Application.getInstance().getPreferences().setValue("info/send_slice_info", enabled) self._application.getPreferences().setValue("info/send_slice_info", enabled)
def _getUserModifiedSettingKeys(self) -> list: def _getUserModifiedSettingKeys(self) -> list:
from cura.CuraApplication import CuraApplication machine_manager = self._application.getMachineManager()
application = cast(CuraApplication, Application.getInstance())
machine_manager = application.getMachineManager()
global_stack = machine_manager.activeMachine global_stack = machine_manager.activeMachine
user_modified_setting_keys = set() # type: Set[str] user_modified_setting_keys = set() # type: Set[str]
@ -106,30 +105,28 @@ class SliceInfo(QObject, Extension):
def _onWriteStarted(self, output_device): def _onWriteStarted(self, output_device):
try: try:
if not Application.getInstance().getPreferences().getValue("info/send_slice_info"): if not self._application.getPreferences().getValue("info/send_slice_info"):
Logger.log("d", "'info/send_slice_info' is turned off.") Logger.log("d", "'info/send_slice_info' is turned off.")
return # Do nothing, user does not want to send data return # Do nothing, user does not want to send data
from cura.CuraApplication import CuraApplication machine_manager = self._application.getMachineManager()
application = cast(CuraApplication, Application.getInstance()) print_information = self._application.getPrintInformation()
machine_manager = application.getMachineManager()
print_information = application.getPrintInformation()
global_stack = machine_manager.activeMachine global_stack = machine_manager.activeMachine
data = dict() # The data that we're going to submit. data = dict() # The data that we're going to submit.
data["time_stamp"] = time.time() data["time_stamp"] = time.time()
data["schema_version"] = 0 data["schema_version"] = 0
data["cura_version"] = application.getVersion() data["cura_version"] = self._application.getVersion()
data["cura_build_type"] = ApplicationMetadata.CuraBuildType data["cura_build_type"] = ApplicationMetadata.CuraBuildType
active_mode = Application.getInstance().getPreferences().getValue("cura/active_mode") active_mode = self._application.getPreferences().getValue("cura/active_mode")
if active_mode == 0: if active_mode == 0:
data["active_mode"] = "recommended" data["active_mode"] = "recommended"
else: else:
data["active_mode"] = "custom" data["active_mode"] = "custom"
data["camera_view"] = application.getPreferences().getValue("general/camera_perspective_mode") data["camera_view"] = self._application.getPreferences().getValue("general/camera_perspective_mode")
if data["camera_view"] == "orthographic": if data["camera_view"] == "orthographic":
data["camera_view"] = "orthogonal" #The database still only recognises the old name "orthogonal". data["camera_view"] = "orthogonal" #The database still only recognises the old name "orthogonal".
@ -142,7 +139,7 @@ class SliceInfo(QObject, Extension):
machine_settings_changed_by_user = True machine_settings_changed_by_user = True
data["machine_settings_changed_by_user"] = machine_settings_changed_by_user data["machine_settings_changed_by_user"] = machine_settings_changed_by_user
data["language"] = Application.getInstance().getPreferences().getValue("general/language") data["language"] = self._application.getPreferences().getValue("general/language")
data["os"] = {"type": platform.system(), "version": platform.version()} data["os"] = {"type": platform.system(), "version": platform.version()}
data["active_machine"] = {"definition_id": global_stack.definition.getId(), data["active_machine"] = {"definition_id": global_stack.definition.getId(),
@ -184,7 +181,7 @@ class SliceInfo(QObject, Extension):
data["models"] = [] data["models"] = []
# Listing all files placed on the build plate # Listing all files placed on the build plate
for node in DepthFirstIterator(application.getController().getScene().getRoot()): for node in DepthFirstIterator(self._application.getController().getScene().getRoot()):
if node.callDecoration("isSliceable"): if node.callDecoration("isSliceable"):
model = dict() model = dict()
model["hash"] = node.getMeshData().getHash() model["hash"] = node.getMeshData().getHash()
@ -263,10 +260,23 @@ class SliceInfo(QObject, Extension):
# Convert data to bytes # Convert data to bytes
binary_data = json.dumps(data).encode("utf-8") binary_data = json.dumps(data).encode("utf-8")
# Sending slice info non-blocking # Send slice info non-blocking
reportJob = SliceInfoJob(self.info_url, binary_data) network_manager = self._application.getHttpRequestManager()
reportJob.start() network_manager.post(self.info_url, data = binary_data,
callback = self._onRequestFinished, error_callback = self._onRequestError)
except Exception: except Exception:
# We really can't afford to have a mistake here, as this would break the sending of g-code to a device # We really can't afford to have a mistake here, as this would break the sending of g-code to a device
# (Either saving or directly to a printer). The functionality of the slice data is not *that* important. # (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
Logger.logException("e", "Exception raised while sending slice info.") # But we should be notified about these problems of course. Logger.logException("e", "Exception raised while sending slice info.") # But we should be notified about these problems of course.
def _onRequestFinished(self, reply: "QNetworkReply") -> None:
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
if status_code == 200:
Logger.log("i", "SliceInfo sent successfully")
return
data = reply.readAll().data().decode("utf-8")
Logger.log("e", "SliceInfo request failed, status code %s, data: %s", status_code, data)
def _onRequestError(self, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None:
Logger.log("e", "Got error for SliceInfo request: %s", reply.errorString())

View file

@ -1,43 +0,0 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Job import Job
from UM.Logger import Logger
from UM.Platform import Platform
import ssl
import urllib.request
import urllib.error
import certifi
class SliceInfoJob(Job):
def __init__(self, url, data):
super().__init__()
self._url = url
self._data = data
def run(self):
if not self._url or not self._data:
Logger.log("e", "URL or DATA for sending slice info was not set!")
return
# CURA-6698 Create an SSL context and use certifi CA certificates for verification.
context = ssl.SSLContext(protocol = ssl.PROTOCOL_TLSv1_2)
context.load_verify_locations(cafile = certifi.where())
# Submit data
kwoptions = {"data": self._data,
"timeout": 5,
"context": context}
Logger.log("i", "Sending anonymous slice info to [%s]...", self._url)
try:
f = urllib.request.urlopen(self._url, **kwoptions)
Logger.log("i", "Sent anonymous slice info.")
f.close()
except urllib.error.HTTPError:
Logger.logException("e", "An HTTP error occurred while trying to send slice information")
except Exception: # We don't want any exception to cause problems
Logger.logException("e", "An exception occurred while trying to send slice information")

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