diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 481b8c2dc8..46b1aa2d71 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -4,6 +4,7 @@ from typing import List from UM.Application import Application from UM.Job import Job +from UM.Logger import Logger from UM.Message import Message from UM.Scene.SceneNode import SceneNode from UM.i18n import i18nCatalog @@ -27,10 +28,14 @@ class ArrangeObjectsJob(Job): title = i18n_catalog.i18nc("@info:title", "Finding Location")) status_message.show() - found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes) + found_solution_for_all = None + try: + found_solution_for_all = arrange(self._nodes, Application.getInstance().getBuildVolume(), self._fixed_nodes) + except: # If the thread crashes, the message should still close + Logger.logException("e", "Unable to arrange the objects on the buildplate. The arrange algorithm has crashed.") status_message.hide() - if not found_solution_for_all: + if found_solution_for_all is not None and not found_solution_for_all: no_full_solution_message = Message( i18n_catalog.i18nc("@info:status", "Unable to find a location within the build volume for all objects"), diff --git a/cura/Arranging/Nest2DArrange.py b/cura/Arranging/Nest2DArrange.py index 93d0788970..727504c9ff 100644 --- a/cura/Arranging/Nest2DArrange.py +++ b/cura/Arranging/Nest2DArrange.py @@ -3,6 +3,7 @@ from pynest2d import Point, Box, Item, NfpConfig, nest from typing import List, TYPE_CHECKING, Optional, Tuple from UM.Application import Application +from UM.Logger import Logger from UM.Math.Matrix import Matrix from UM.Math.Polygon import Polygon from UM.Math.Quaternion import Quaternion @@ -44,6 +45,9 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV node_items = [] for node in nodes_to_arrange: hull_polygon = node.callDecoration("getConvexHull") + if not hull_polygon or hull_polygon.getPoints is None: + Logger.log("w", "Object {} cannot be arranged because it has no convex hull.".format(node.getName())) + continue converted_points = [] for point in hull_polygon.getPoints(): converted_points.append(Point(point[0] * factor, point[1] * factor)) diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py index e825afd2a9..b4ea2d8382 100644 --- a/cura/OAuth2/AuthorizationHelpers.py +++ b/cura/OAuth2/AuthorizationHelpers.py @@ -1,11 +1,11 @@ -# Copyright (c) 2019 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from datetime import datetime import json import random from hashlib import sha512 from base64 import b64encode -from typing import Optional +from typing import Optional, Any, Dict, Tuple import requests @@ -16,6 +16,7 @@ from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settin catalog = i18nCatalog("cura") TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S" + class AuthorizationHelpers: """Class containing several helpers to deal with the authorization flow.""" @@ -121,10 +122,13 @@ class AuthorizationHelpers: if not user_data or not isinstance(user_data, dict): Logger.log("w", "Could not parse user data from token: %s", user_data) return None + return UserProfile( user_id = user_data["user_id"], username = user_data["username"], - profile_image_url = user_data.get("profile_image_url", "") + profile_image_url = user_data.get("profile_image_url", ""), + organization_id = user_data.get("organization", {}).get("organization_id", ""), + subscriptions = user_data.get("subscriptions", []) ) @staticmethod diff --git a/cura/OAuth2/Models.py b/cura/OAuth2/Models.py index 93b44e8057..f49fdc1421 100644 --- a/cura/OAuth2/Models.py +++ b/cura/OAuth2/Models.py @@ -1,6 +1,6 @@ -# Copyright (c) 2019 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, Dict, Any +from typing import Optional, Dict, Any, List class BaseModel: @@ -27,6 +27,8 @@ class UserProfile(BaseModel): user_id = None # type: Optional[str] username = None # type: Optional[str] profile_image_url = None # type: Optional[str] + organization_id = None # type: Optional[str] + subscriptions = None # type: Optional[List[Dict[str, Any]]] class AuthenticationResponse(BaseModel): diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 94a1869598..58fd8171b5 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -998,6 +998,11 @@ class MachineManager(QObject): self.activeMaterialChanged.emit() self.activeIntentChanged.emit() + # Force an update of resolve values + property_names = ["resolve", "validationState"] + for setting_key in self._global_container_stack.getAllKeys(): + self._global_container_stack.propertiesChanged.emit(setting_key, property_names) + def _onMaterialNameChanged(self) -> None: self.activeMaterialChanged.emit() diff --git a/cura/UltimakerCloud/UltimakerCloudScope.py b/cura/UltimakerCloud/UltimakerCloudScope.py index 0e9adaf2e7..5477423099 100644 --- a/cura/UltimakerCloud/UltimakerCloudScope.py +++ b/cura/UltimakerCloud/UltimakerCloudScope.py @@ -7,10 +7,9 @@ from cura.CuraApplication import CuraApplication class UltimakerCloudScope(DefaultUserAgentScope): - """Add an Authorization header to the request for Ultimaker Cloud Api requests. - - When the user is not logged in or a token is not available, a warning will be logged - Also add the user agent headers (see DefaultUserAgentScope) + """ + Add an Authorization header to the request for Ultimaker Cloud Api requests, if available. + Also add the user agent headers (see DefaultUserAgentScope). """ def __init__(self, application: CuraApplication): @@ -22,7 +21,7 @@ class UltimakerCloudScope(DefaultUserAgentScope): super().requestHook(request) token = self._account.accessToken if not self._account.isLoggedIn or token is None: - Logger.warning("Cannot add authorization to Cloud Api request") + Logger.debug("User is not logged in for Cloud API request to {url}".format(url = request.url().toDisplayString())) return header_dict = { diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 2c29728d66..c9e3689a0d 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -19,6 +19,7 @@ from UM.Scene.SceneNode import SceneNode # For typing. from cura.CuraApplication import CuraApplication from cura.Machines.ContainerTree import ContainerTree from cura.Scene.BuildPlateDecorator import BuildPlateDecorator +from cura.Scene.ConvexHullDecorator import ConvexHullDecorator from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator from cura.Scene.ZOffsetDecorator import ZOffsetDecorator @@ -108,6 +109,7 @@ class ThreeMFReader(MeshReader): um_node = CuraSceneNode() # This adds a SettingOverrideDecorator um_node.addDecorator(BuildPlateDecorator(active_build_plate)) + um_node.addDecorator(ConvexHullDecorator()) um_node.setName(node_name) um_node.setId(node_id) transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation()) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 519d302618..4b196f7b5d 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -82,7 +82,7 @@ class CuraEngineBackend(QObject, Backend): default_engine_location = execpath break - self._application = CuraApplication.getInstance() #type: CuraApplication + application = CuraApplication.getInstance() #type: CuraApplication self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel] self._machine_error_checker = None #type: Optional[MachineErrorChecker] @@ -92,7 +92,7 @@ class CuraEngineBackend(QObject, Backend): Logger.log("i", "Found CuraEngine at: %s", default_engine_location) default_engine_location = os.path.abspath(default_engine_location) - self._application.getPreferences().addPreference("backend/location", default_engine_location) + application.getPreferences().addPreference("backend/location", default_engine_location) # Workaround to disable layer view processing if layer view is not active. self._layer_view_active = False #type: bool @@ -101,7 +101,7 @@ class CuraEngineBackend(QObject, Backend): self._stored_layer_data = [] # type: List[Arcus.PythonMessage] self._stored_optimized_layer_data = {} # type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob - self._scene = self._application.getController().getScene() #type: Scene + self._scene = application.getController().getScene() #type: Scene self._scene.sceneChanged.connect(self._onSceneChanged) # Triggers for auto-slicing. Auto-slicing is triggered as follows: @@ -141,7 +141,7 @@ class CuraEngineBackend(QObject, Backend): self._slice_start_time = None #type: Optional[float] self._is_disabled = False #type: bool - self._application.getPreferences().addPreference("general/auto_slice", False) + application.getPreferences().addPreference("general/auto_slice", False) self._use_timer = False #type: bool # When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired. @@ -151,19 +151,20 @@ class CuraEngineBackend(QObject, Backend): self._change_timer.setSingleShot(True) self._change_timer.setInterval(500) self.determineAutoSlicing() - self._application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged) + application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged) - self._application.initializationFinished.connect(self.initialize) + application.initializationFinished.connect(self.initialize) def initialize(self) -> None: - self._multi_build_plate_model = self._application.getMultiBuildPlateModel() + application = CuraApplication.getInstance() + self._multi_build_plate_model = application.getMultiBuildPlateModel() - self._application.getController().activeViewChanged.connect(self._onActiveViewChanged) + application.getController().activeViewChanged.connect(self._onActiveViewChanged) if self._multi_build_plate_model: self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged) - self._application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged) + application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() # extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash @@ -173,10 +174,10 @@ class CuraEngineBackend(QObject, Backend): self.backendConnected.connect(self._onBackendConnected) # When a tool operation is in progress, don't slice. So we need to listen for tool operations. - self._application.getController().toolOperationStarted.connect(self._onToolOperationStarted) - self._application.getController().toolOperationStopped.connect(self._onToolOperationStopped) + application.getController().toolOperationStarted.connect(self._onToolOperationStarted) + application.getController().toolOperationStopped.connect(self._onToolOperationStopped) - self._machine_error_checker = self._application.getMachineErrorChecker() + self._machine_error_checker = application.getMachineErrorChecker() self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished) def close(self) -> None: @@ -195,7 +196,7 @@ class CuraEngineBackend(QObject, Backend): This is useful for debugging and used to actually start the engine. :return: list of commands and args / parameters. """ - command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""] + command = [CuraApplication.getInstance().getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""] parser = argparse.ArgumentParser(prog = "cura", add_help = False) parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.") @@ -259,7 +260,8 @@ class CuraEngineBackend(QObject, Backend): self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here. # see if we really have to slice - active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate + application = CuraApplication.getInstance() + active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0) Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced) num_objects = self._numObjectsPerBuildPlate() @@ -274,8 +276,8 @@ class CuraEngineBackend(QObject, Backend): self.slice() return self._stored_optimized_layer_data[build_plate_to_be_sliced] = [] - if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate: - self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced) + if application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate: + application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced) if self._process is None: # type: ignore self._createSocket() @@ -314,7 +316,7 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(0) Logger.log("d", "Attempting to kill the engine process") - if self._application.getUseExternalBackend(): + if CuraApplication.getInstance().getUseExternalBackend(): return if self._process is not None: # type: ignore @@ -350,8 +352,9 @@ class CuraEngineBackend(QObject, Backend): self.backendError.emit(job) return + application = CuraApplication.getInstance() if job.getResult() == StartJobResult.MaterialIncompatible: - if self._application.platformActivity: + if application.platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() @@ -362,7 +365,7 @@ class CuraEngineBackend(QObject, Backend): return if job.getResult() == StartJobResult.SettingError: - if self._application.platformActivity: + if application.platformActivity: if not self._global_container_stack: Logger.log("w", "Global container stack not assigned to CuraEngineBackend!") return @@ -394,7 +397,7 @@ class CuraEngineBackend(QObject, Backend): elif job.getResult() == StartJobResult.ObjectSettingError: errors = {} - for node in DepthFirstIterator(self._application.getController().getScene().getRoot()): + for node in DepthFirstIterator(application.getController().getScene().getRoot()): stack = node.callDecoration("getStack") if not stack: continue @@ -415,7 +418,7 @@ class CuraEngineBackend(QObject, Backend): return if job.getResult() == StartJobResult.BuildPlateError: - if self._application.platformActivity: + if application.platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() @@ -433,7 +436,7 @@ class CuraEngineBackend(QObject, Backend): return if job.getResult() == StartJobResult.NothingToSlice: - if self._application.platformActivity: + if application.platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Please review settings and check if your models:" "\n- Fit within the build volume" "\n- Are assigned to an enabled extruder" @@ -466,7 +469,7 @@ class CuraEngineBackend(QObject, Backend): enable_timer = True self._is_disabled = False - if not self._application.getPreferences().getValue("general/auto_slice"): + if not CuraApplication.getInstance().getPreferences().getValue("general/auto_slice"): enable_timer = False for node in DepthFirstIterator(self._scene.getRoot()): if node.callDecoration("isBlockSlicing"): @@ -560,7 +563,7 @@ class CuraEngineBackend(QObject, Backend): :param error: The exception that occurred. """ - if self._application.isShuttingDown(): + if CuraApplication.getInstance().isShuttingDown(): return super()._onSocketError(error) @@ -600,7 +603,7 @@ class CuraEngineBackend(QObject, Backend): cast(SceneNode, node.getParent()).removeChild(node) def markSliceAll(self) -> None: - for build_plate_number in range(self._application.getMultiBuildPlateModel().maxBuildPlate + 1): + for build_plate_number in range(CuraApplication.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1): if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) @@ -696,12 +699,13 @@ class CuraEngineBackend(QObject, Backend): gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically. except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. gcode_list = [] + application = CuraApplication.getInstance() for index, line in enumerate(gcode_list): - replaced = line.replace("{print_time}", str(self._application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601))) - replaced = replaced.replace("{filament_amount}", str(self._application.getPrintInformation().materialLengths)) - replaced = replaced.replace("{filament_weight}", str(self._application.getPrintInformation().materialWeights)) - replaced = replaced.replace("{filament_cost}", str(self._application.getPrintInformation().materialCosts)) - replaced = replaced.replace("{jobname}", str(self._application.getPrintInformation().jobName)) + replaced = line.replace("{print_time}", str(application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601))) + replaced = replaced.replace("{filament_amount}", str(application.getPrintInformation().materialLengths)) + replaced = replaced.replace("{filament_weight}", str(application.getPrintInformation().materialWeights)) + replaced = replaced.replace("{filament_cost}", str(application.getPrintInformation().materialCosts)) + replaced = replaced.replace("{jobname}", str(application.getPrintInformation().jobName)) gcode_list[index] = replaced @@ -711,7 +715,7 @@ class CuraEngineBackend(QObject, Backend): Logger.log("d", "Number of models per buildplate: %s", dict(self._numObjectsPerBuildPlate())) # See if we need to process the sliced layers job. - active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate + active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate if ( self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and @@ -870,9 +874,9 @@ class CuraEngineBackend(QObject, Backend): def _onActiveViewChanged(self) -> None: """Called when the user changes the active view mode.""" - view = self._application.getController().getActiveView() + view = CuraApplication.getInstance().getController().getActiveView() if view: - active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate + active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet. self._layer_view_active = True # There is data and we're not slicing at the moment @@ -909,7 +913,7 @@ class CuraEngineBackend(QObject, Backend): extruder.propertyChanged.disconnect(self._onSettingChanged) extruder.containersChanged.disconnect(self._onChanged) - self._global_container_stack = self._application.getMachineManager().activeMachine + self._global_container_stack = CuraApplication.getInstance().getMachineManager().activeMachine if self._global_container_stack: self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed. diff --git a/plugins/PostProcessingPlugin/__init__.py b/plugins/PostProcessingPlugin/__init__.py index 6ddecfac69..019627ebd5 100644 --- a/plugins/PostProcessingPlugin/__init__.py +++ b/plugins/PostProcessingPlugin/__init__.py @@ -7,6 +7,7 @@ # tries to create PyQt objects on a non-main thread. import Arcus # @UnusedImport import Savitar # @UnusedImport +import pynest2d # @UnusedImport from . import PostProcessingPlugin diff --git a/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py b/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py index 70fda32692..53a6c30ccf 100644 --- a/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py @@ -13,7 +13,7 @@ from ..PostProcessingPlugin import PostProcessingPlugin # not sure if needed sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) -""" In this file, commnunity refers to regular Cura for makers.""" +""" In this file, community refers to regular Cura for makers.""" mock_plugin_registry = MagicMock() mock_plugin_registry.getPluginPath = MagicMock(return_value = "mocked_plugin_path") diff --git a/plugins/SliceInfoPlugin/SliceInfo.py b/plugins/SliceInfoPlugin/SliceInfo.py index 284389064c..61fc777290 100755 --- a/plugins/SliceInfoPlugin/SliceInfo.py +++ b/plugins/SliceInfoPlugin/SliceInfo.py @@ -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. import json @@ -116,6 +116,7 @@ class SliceInfo(QObject, Extension): machine_manager = self._application.getMachineManager() print_information = self._application.getPrintInformation() + user_profile = self._application.getCuraAPI().account.userProfile global_stack = machine_manager.activeMachine @@ -124,6 +125,8 @@ class SliceInfo(QObject, Extension): data["schema_version"] = 0 data["cura_version"] = self._application.getVersion() data["cura_build_type"] = ApplicationMetadata.CuraBuildType + data["organization_id"] = user_profile.get("organization_id", None) if user_profile else None + data["subscriptions"] = user_profile.get("subscriptions", []) if user_profile else [] active_mode = self._application.getPreferences().getValue("cura/active_mode") if active_mode == 0: diff --git a/plugins/SliceInfoPlugin/example_data.html b/plugins/SliceInfoPlugin/example_data.html index 103eb55a6a..b349ec328d 100644 --- a/plugins/SliceInfoPlugin/example_data.html +++ b/plugins/SliceInfoPlugin/example_data.html @@ -1,12 +1,17 @@
- Cura Version: 4.0