Browse Source

Merge branch 'main' into PP-428-Improve-TPU-top-surface

Casper Lamboo 1 year ago
parent
commit
4896d2572c

+ 1 - 1
.github/ISSUE_TEMPLATE/SlicingCrash.yaml

@@ -4,7 +4,7 @@ labels: ["Type: Bug", "Status: Triage", "Slicing Error :collision:"]
 body:
 - type: markdown
   attributes:
-    value: |
+    value: |      
        ### 💥 Slicing Crash Analysis Tool 💥
        We are taking steps to analyze an increase in reported crashes more systematically. We'll need some help with that. 😇
        Before filling out the report below, we want you to try a special Cura 5.7 Alpha.

+ 1 - 1
.github/workflows/unit-test.yml

@@ -55,7 +55,7 @@ jobs:
     needs: [ conan-recipe-version ]
     with:
       recipe_id_full: ${{ needs.conan-recipe-version.outputs.recipe_id_full }}
-      conan_extra_args: '-g VirtualPythonEnv -o cura:devtools=True -c tools.build:skip_test=False'
+      conan_extra_args: '-g VirtualPythonEnv -o cura:devtools=True -c tools.build:skip_test=False --options "*:enable_sentry=False"'
       unit_test_cmd: 'pytest --junitxml=junit_cura.xml'
       unit_test_dir: 'tests'
       conan_generator_dir: './venv/bin'

+ 1 - 0
conanfile.py

@@ -350,6 +350,7 @@ class CuraConan(ConanFile):
         self.requires("cpython/3.10.4@ultimaker/stable")
         self.requires("clipper/6.4.2@ultimaker/stable")
         self.requires("openssl/3.2.0")
+        self.requires("protobuf/3.21.12")
         self.requires("boost/1.82.0")
         self.requires("spdlog/1.12.0")
         self.requires("fmt/10.1.1")

+ 4 - 2
cura/CuraActions.py

@@ -1,6 +1,5 @@
 # Copyright (c) 2023 UltiMaker
 # Cura is released under the terms of the LGPLv3 or higher.
-
 from typing import List, cast
 
 from PyQt6.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty
@@ -33,7 +32,6 @@ from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOper
 from UM.Logger import Logger
 from UM.Scene.SceneNode import SceneNode
 
-
 class CuraActions(QObject):
     def __init__(self, parent: QObject = None) -> None:
         super().__init__(parent)
@@ -273,7 +271,11 @@ class CuraActions(QObject):
         # deselect currently selected nodes, and select the new nodes
         for node in Selection.getAllSelectedObjects():
             Selection.remove(node)
+
+        numberOfFixedNodes = len(fixed_nodes)
         for node in nodes:
+            numberOfFixedNodes += 1
+            node.printOrder = numberOfFixedNodes
             Selection.add(node)
 
     def _openUrl(self, url: QUrl) -> None:

+ 34 - 0
cura/CuraApplication.py

@@ -617,6 +617,7 @@ class CuraApplication(QtApplication):
 
         preferences.addPreference("view/invert_zoom", False)
         preferences.addPreference("view/filter_current_build_plate", False)
+        preferences.addPreference("view/navigation_style", "cura")
         preferences.addPreference("cura/sidebar_collapsed", False)
 
         preferences.addPreference("cura/favorite_materials", "")
@@ -1082,6 +1083,10 @@ class CuraApplication(QtApplication):
     def getTextManager(self, *args) -> "TextManager":
         return self._text_manager
 
+    @pyqtSlot()
+    def setWorkplaceDropToBuildplate(self):
+        return self._physics.setAppAllModelDropDown()
+
     def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
         if self._cura_formula_functions is None:
             self._cura_formula_functions = CuraFormulaFunctions(self)
@@ -1137,6 +1142,16 @@ class CuraApplication(QtApplication):
             self._build_plate_model = BuildPlateModel(self)
         return self._build_plate_model
 
+    @pyqtSlot()
+    def exportUcp(self):
+        writer = self.getMeshFileHandler().getWriter("3MFWriter")
+
+        if writer is None:
+            Logger.warning("3mf writer is not enabled")
+            return
+
+        writer.exportUcp()
+
     def getCuraSceneController(self, *args) -> CuraSceneController:
         if self._cura_scene_controller is None:
             self._cura_scene_controller = CuraSceneController.createCuraSceneController()
@@ -1962,6 +1977,17 @@ class CuraApplication(QtApplication):
 
     openProjectFile = pyqtSignal(QUrl, bool, arguments = ["project_file", "add_to_recent_files"])  # Emitted when a project file is about to open.
 
+    @pyqtSlot(QUrl, bool)
+    def readLocalUcpFile(self, file: QUrl, add_to_recent_files: bool = True):
+
+        file_name = QUrl(file).toLocalFile()
+        workspace_reader = self.getWorkspaceFileHandler()
+        if workspace_reader is None:
+            Logger.warning(f"Workspace reader not found, cannot read file {file_name}.")
+            return
+
+        workspace_reader.readLocalFile(file, add_to_recent_files)
+
     @pyqtSlot(QUrl, str, bool)
     @pyqtSlot(QUrl, str)
     @pyqtSlot(QUrl)
@@ -2167,6 +2193,12 @@ class CuraApplication(QtApplication):
     def addNonSliceableExtension(self, extension):
         self._non_sliceable_extensions.append(extension)
 
+    @pyqtSlot(str, result = bool)
+    def isProjectUcp(self, file_url) -> bool:
+        file_path = QUrl(file_url).toLocalFile()
+        workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path)
+        return workspace_reader.getIsProjectUcp()
+
     @pyqtSlot(str, result=bool)
     def checkIsValidProjectFile(self, file_url):
         """Checks if the given file URL is a valid project file. """
@@ -2176,6 +2208,8 @@ class CuraApplication(QtApplication):
         if workspace_reader is None:
             return False  # non-project files won't get a reader
         try:
+            if workspace_reader.getPluginId() == "3MFReader":
+                workspace_reader.clearOpenAsUcp()
             result = workspace_reader.preRead(file_path, show_dialog=False)
             return result == WorkspaceReader.PreReadResult.accepted
         except:

+ 8 - 3
cura/Machines/Models/MachineListModel.py

@@ -5,7 +5,7 @@
 # online cloud connected printers are represented within this ListModel. Additional information such as the number of
 # connected printers for each printer type is gathered.
 
-from typing import Optional, List, cast
+from typing import Optional, List, cast, Dict, Any
 
 from PyQt6.QtCore import Qt, QTimer, QObject, pyqtSlot, pyqtProperty, pyqtSignal
 
@@ -30,10 +30,10 @@ class MachineListModel(ListModel):
     ComponentTypeRole = Qt.ItemDataRole.UserRole + 8
     IsNetworkedMachineRole = Qt.ItemDataRole.UserRole + 9
 
-    def __init__(self, parent: Optional[QObject] = None, machines_filter: List[GlobalStack] = None, listenToChanges: bool = True) -> None:
+    def __init__(self, parent: Optional[QObject] = None, machines_filter: List[GlobalStack] = None, listenToChanges: bool = True, showCloudPrinters: bool = False) -> None:
         super().__init__(parent)
 
-        self._show_cloud_printers = False
+        self._show_cloud_printers = showCloudPrinters
         self._machines_filter = machines_filter
 
         self._catalog = i18nCatalog("cura")
@@ -159,3 +159,8 @@ class MachineListModel(ListModel):
             "machineCount": machine_count,
             "catergory": "connected" if is_online else "other",
         })
+
+    def getItems(self) -> Dict[str, Any]:
+        if self.count > 0:
+            return self.items
+        return {}

+ 8 - 3
cura/OAuth2/AuthorizationHelpers.py

@@ -16,6 +16,7 @@ from UM.TaskManagement.HttpRequestManager import HttpRequestManager  # To downlo
 
 catalog = i18nCatalog("cura")
 TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
+REQUEST_TIMEOUT = 5 # Seconds
 
 
 class AuthorizationHelpers:
@@ -53,7 +54,8 @@ class AuthorizationHelpers:
             data = urllib.parse.urlencode(data).encode("UTF-8"),
             headers_dict = headers,
             callback = lambda response: self.parseTokenResponse(response, callback),
-            error_callback = lambda response, _: self.parseTokenResponse(response, callback)
+            error_callback = lambda response, _: self.parseTokenResponse(response, callback),
+            timeout = REQUEST_TIMEOUT
         )
 
     def getAccessTokenUsingRefreshToken(self, refresh_token: str, callback: Callable[[AuthenticationResponse], None]) -> None:
@@ -77,7 +79,9 @@ class AuthorizationHelpers:
             data = urllib.parse.urlencode(data).encode("UTF-8"),
             headers_dict = headers,
             callback = lambda response: self.parseTokenResponse(response, callback),
-            error_callback = lambda response, _: self.parseTokenResponse(response, callback)
+            error_callback = lambda response, _: self.parseTokenResponse(response, callback),
+            urgent = True,
+            timeout = REQUEST_TIMEOUT
         )
 
     def parseTokenResponse(self, token_response: QNetworkReply, callback: Callable[[AuthenticationResponse], None]) -> None:
@@ -122,7 +126,8 @@ class AuthorizationHelpers:
             check_token_url,
             headers_dict = headers,
             callback = lambda reply: self._parseUserProfile(reply, success_callback, failed_callback),
-            error_callback = lambda _, _2: failed_callback() if failed_callback is not None else None
+            error_callback = lambda _, _2: failed_callback() if failed_callback is not None else None,
+            timeout = REQUEST_TIMEOUT
         )
 
     def _parseUserProfile(self, reply: QNetworkReply, success_callback: Optional[Callable[[UserProfile], None]], failed_callback: Optional[Callable[[], None]] = None) -> None:

+ 26 - 4
cura/OAuth2/AuthorizationService.py

@@ -1,4 +1,4 @@
-# Copyright (c) 2021 Ultimaker B.V.
+# Copyright (c) 2024 UltiMaker
 # Cura is released under the terms of the LGPLv3 or higher.
 
 import json
@@ -6,13 +6,14 @@ from datetime import datetime, timedelta
 from typing import Callable, Dict, Optional, TYPE_CHECKING, Union
 from urllib.parse import urlencode, quote_plus
 
-from PyQt6.QtCore import QUrl
+from PyQt6.QtCore import QUrl, QTimer
 from PyQt6.QtGui import QDesktopServices
 
 from UM.Logger import Logger
 from UM.Message import Message
 from UM.Signal import Signal
 from UM.i18n import i18nCatalog
+from UM.TaskManagement.HttpRequestManager import HttpRequestManager  # To download log-in tokens.
 from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
 from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
 from cura.OAuth2.Models import AuthenticationResponse, BaseModel
@@ -25,6 +26,8 @@ if TYPE_CHECKING:
 
 MYCLOUD_LOGOFF_URL = "https://account.ultimaker.com/logoff?utm_source=cura&utm_medium=software&utm_campaign=change-account-before-adding-printers"
 
+REFRESH_TOKEN_MAX_RETRIES = 15
+REFRESH_TOKEN_RETRY_INTERVAL = 1000
 
 class AuthorizationService:
     """The authorization service is responsible for handling the login flow, storing user credentials and providing
@@ -57,6 +60,12 @@ class AuthorizationService:
 
         self.onAuthStateChanged.connect(self._authChanged)
 
+        self._refresh_token_retries = 0
+        self._refresh_token_retry_timer = QTimer()
+        self._refresh_token_retry_timer.setInterval(REFRESH_TOKEN_RETRY_INTERVAL)
+        self._refresh_token_retry_timer.setSingleShot(True)
+        self._refresh_token_retry_timer.timeout.connect(self.refreshAccessToken)
+
     def _authChanged(self, logged_in):
         if logged_in and self._unable_to_get_data_message is not None:
             self._unable_to_get_data_message.hide()
@@ -167,16 +176,29 @@ class AuthorizationService:
             return
 
         def process_auth_data(response: AuthenticationResponse) -> None:
+            self._currently_refreshing_token = False
+
             if response.success:
+                self._refresh_token_retries = 0
                 self._storeAuthData(response)
+                HttpRequestManager.getInstance().setDelayRequests(False)
                 self.onAuthStateChanged.emit(logged_in = True)
             else:
-                Logger.warning("Failed to get a new access token from the server.")
-                self.onAuthStateChanged.emit(logged_in = False)
+                if self._refresh_token_retries >= REFRESH_TOKEN_MAX_RETRIES:
+                    self._refresh_token_retries = 0
+                    Logger.warning("Failed to get a new access token from the server, giving up.")
+                    HttpRequestManager.getInstance().setDelayRequests(False)
+                    self.onAuthStateChanged.emit(logged_in = False)
+                else:
+                    # Retry a bit later, network may be offline right now and will hopefully be back soon
+                    Logger.warning("Failed to get a new access token from the server, retrying later.")
+                    self._refresh_token_retries += 1
+                    self._refresh_token_retry_timer.start()
 
         if self._currently_refreshing_token:
             Logger.debug("Was already busy refreshing token. Do not start a new request.")
             return
+        HttpRequestManager.getInstance().setDelayRequests(True)
         self._currently_refreshing_token = True
         self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token, process_auth_data)
 

+ 9 - 2
cura/PlatformPhysics.py

@@ -39,6 +39,11 @@ class PlatformPhysics:
 
         Application.getInstance().getPreferences().addPreference("physics/automatic_push_free", False)
         Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True)
+        self._app_all_model_drop = False
+
+    def setAppAllModelDropDown(self):
+        self._app_all_model_drop = True
+        self._onChangeTimerFinished()
 
     def _onSceneChanged(self, source):
         if not source.callDecoration("isSliceable"):
@@ -80,9 +85,9 @@ class PlatformPhysics:
             # Move it downwards if bottom is above platform
             move_vector = Vector()
 
-            if node.getSetting(SceneNodeSettings.AutoDropDown, app_automatic_drop_down) and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root) and node.isEnabled(): #If an object is grouped, don't move it down
+            if (node.getSetting(SceneNodeSettings.AutoDropDown, app_automatic_drop_down) or self._app_all_model_drop) and not (node.getParent() and node.getParent().callDecoration("isGroup") or node.getParent() != root)  and node.isEnabled():
                 z_offset = node.callDecoration("getZOffset") if node.getDecorator(ZOffsetDecorator.ZOffsetDecorator) else 0
-                move_vector = move_vector.set(y = -bbox.bottom + z_offset)
+                move_vector = move_vector.set(y=-bbox.bottom + z_offset)
 
             # If there is no convex hull for the node, start calculating it and continue.
             if not node.getDecorator(ConvexHullDecorator) and not node.callDecoration("isNonPrintingMesh") and node.callDecoration("getLayerData") is None:
@@ -168,6 +173,8 @@ class PlatformPhysics:
                 op = PlatformPhysicsOperation.PlatformPhysicsOperation(node, move_vector)
                 op.push()
 
+        # setting this drop to model same as app_automatic_drop_down
+        self._app_all_model_drop = False
         # After moving, we have to evaluate the boundary checks for nodes
         build_volume.updateNodeBoundaryCheck()
 

+ 3 - 0
cura/PrintOrderManager.py

@@ -116,6 +116,9 @@ class PrintOrderManager(QObject):
                                      ) -> (Optional[CuraSceneNode], Optional[CuraSceneNode], Optional[CuraSceneNode]):
         nodes = self._get_nodes()
         ordered_nodes = sorted(nodes, key=lambda n: n.printOrder)
+        for i, node in enumerate(ordered_nodes, 1):
+            node.printOrder = i
+
         selected_node = PrintOrderManager._getSingleSelectedNode()
         if selected_node and selected_node in ordered_nodes:
             selected_node_index = ordered_nodes.index(selected_node)

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