Browse Source

Merge branch 'Ultimaker-master'

Eryone 3 years ago
parent
commit
678eaeea89
10 changed files with 106 additions and 52 deletions
  1. 5 0
      .gitignore
  2. 2 2
      CITATION.cff
  3. 1 1
      README.md
  4. BIN
      cura-logo.PNG
  5. 28 23
      cura/API/Account.py
  6. 6 2
      cura/ApplicationMetadata.py
  7. 2 2
      cura/Arranging/Nest2DArrange.py
  8. 5 2
      cura/Backups/Backup.py
  9. 47 19
      cura/BuildVolume.py
  10. 10 1
      cura/CrashHandler.py

+ 5 - 0
.gitignore

@@ -56,6 +56,11 @@ plugins/SettingsGuide
 plugins/SettingsGuide2
 plugins/SVGToolpathReader
 plugins/X3GWriter
+plugins/CuraFlatPack
+plugins/CuraRemoteSupport
+plugins/ModelCutter
+plugins/PrintProfileCreator
+plugins/MultiPrintPlugin
 
 #Build stuff
 CMakeCache.txt

+ 2 - 2
CITATION.cff

@@ -7,5 +7,5 @@ license: "LGPL-3.0"
 message: "If you use this software, please cite it using these metadata."
 repository-code: "https://github.com/ultimaker/cura/"
 title: "Ultimaker Cura"
-version: "4.10.0"
-...
+version: "4.12.0"
+...

+ 1 - 1
README.md

@@ -2,7 +2,7 @@ Cura
 ====
 Ultimaker Cura is a state-of-the-art slicer application to prepare your 3D models for printing with a 3D printer. With hundreds of settings and hundreds of community-managed print profiles, Ultimaker Cura is sure to lead your next project to a success.
 
-![Screenshot](screenshot.png)
+![Screenshot](cura-logo.PNG)
 
 Logging Issues
 ------------

BIN
cura-logo.PNG


+ 28 - 23
cura/API/Account.py

@@ -1,15 +1,15 @@
-# Copyright (c) 2018 Ultimaker B.V.
+# Copyright (c) 2021 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
-from datetime import datetime
-from typing import Any, Optional, Dict, TYPE_CHECKING, Callable
 
+from datetime import datetime
 from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty, QTimer, Q_ENUMS
+from typing import Any, Optional, Dict, TYPE_CHECKING, Callable
 
 from UM.Logger import Logger
 from UM.Message import Message
 from UM.i18n import i18nCatalog
 from cura.OAuth2.AuthorizationService import AuthorizationService
-from cura.OAuth2.Models import OAuth2Settings
+from cura.OAuth2.Models import OAuth2Settings, UserProfile
 from cura.UltimakerCloud import UltimakerCloudConstants
 
 if TYPE_CHECKING:
@@ -46,6 +46,9 @@ class Account(QObject):
     loginStateChanged = pyqtSignal(bool)
     """Signal emitted when user logged in or out"""
 
+    userProfileChanged = pyqtSignal()
+    """Signal emitted when new account information is available."""
+
     additionalRightsChanged = pyqtSignal("QVariantMap")
     """Signal emitted when a users additional rights change"""
 
@@ -62,7 +65,7 @@ class Account(QObject):
     updatePackagesEnabledChanged = pyqtSignal(bool)
 
     CLIENT_SCOPES = "account.user.read drive.backup.read drive.backup.write packages.download " \
-                    "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write " \
+                    "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write connect.material.write " \
                     "library.project.read library.project.write cura.printjob.read cura.printjob.write " \
                     "cura.mesh.read cura.mesh.write"
 
@@ -71,13 +74,14 @@ class Account(QObject):
         self._application = application
         self._new_cloud_printers_detected = False
 
-        self._error_message = None  # type: Optional[Message]
+        self._error_message: Optional[Message] = None
         self._logged_in = False
+        self._user_profile: Optional[UserProfile] = None
         self._additional_rights: Dict[str, Any] = {}
         self._sync_state = SyncState.IDLE
         self._manual_sync_enabled = False
         self._update_packages_enabled = False
-        self._update_packages_action = None  # type: Optional[Callable]
+        self._update_packages_action: Optional[Callable] = None
         self._last_sync_str = "-"
 
         self._callback_port = 32118
@@ -103,7 +107,7 @@ class Account(QObject):
         self._update_timer.setSingleShot(True)
         self._update_timer.timeout.connect(self.sync)
 
-        self._sync_services = {}  # type: Dict[str, int]
+        self._sync_services: Dict[str, int] = {}
         """contains entries "service_name" : SyncState"""
 
     def initialize(self) -> None:
@@ -196,12 +200,17 @@ class Account(QObject):
             self._logged_in = logged_in
             self.loginStateChanged.emit(logged_in)
             if logged_in:
+                self._authorization_service.getUserProfile(self._onProfileChanged)
                 self._setManualSyncEnabled(False)
                 self._sync()
             else:
                 if self._update_timer.isActive():
                     self._update_timer.stop()
 
+    def _onProfileChanged(self, profile: Optional[UserProfile]) -> None:
+        self._user_profile = profile
+        self.userProfileChanged.emit()
+
     def _sync(self) -> None:
         """Signals all sync services to start syncing
 
@@ -243,32 +252,28 @@ class Account(QObject):
                 return
         self._authorization_service.startAuthorizationFlow(force_logout_before_login)
 
-    @pyqtProperty(str, notify=loginStateChanged)
+    @pyqtProperty(str, notify = userProfileChanged)
     def userName(self):
-        user_profile = self._authorization_service.getUserProfile()
-        if not user_profile:
-            return None
-        return user_profile.username
+        if not self._user_profile:
+            return ""
+        return self._user_profile.username
 
-    @pyqtProperty(str, notify = loginStateChanged)
+    @pyqtProperty(str, notify = userProfileChanged)
     def profileImageUrl(self):
-        user_profile = self._authorization_service.getUserProfile()
-        if not user_profile:
-            return None
-        return user_profile.profile_image_url
+        if not self._user_profile:
+            return ""
+        return self._user_profile.profile_image_url
 
     @pyqtProperty(str, notify=accessTokenChanged)
     def accessToken(self) -> Optional[str]:
         return self._authorization_service.getAccessToken()
 
-    @pyqtProperty("QVariantMap", notify = loginStateChanged)
+    @pyqtProperty("QVariantMap", notify = userProfileChanged)
     def userProfile(self) -> Optional[Dict[str, Optional[str]]]:
         """None if no user is logged in otherwise the logged in  user as a dict containing containing user_id, username and profile_image_url """
-
-        user_profile = self._authorization_service.getUserProfile()
-        if not user_profile:
+        if not self._user_profile:
             return None
-        return user_profile.__dict__
+        return self._user_profile.__dict__
 
     @pyqtProperty(str, notify=lastSyncDateTimeChanged)
     def lastSyncDateTime(self) -> str:

+ 6 - 2
cura/ApplicationMetadata.py

@@ -1,4 +1,4 @@
-# Copyright (c) 2020 Ultimaker B.V.
+# Copyright (c) 2021 Ultimaker B.V.
 # 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
 # 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.
-CuraSDKVersion = "7.8.0"
+CuraSDKVersion = "7.9.0"
 
 try:
     from cura.CuraVersion import CuraAppName  # type: ignore
@@ -46,6 +46,10 @@ except ImportError:
 # Various convenience flags indicating what kind of Cura build it is.
 __ENTERPRISE_VERSION_TYPE = "enterprise"
 IsEnterpriseVersion = CuraBuildType.lower() == __ENTERPRISE_VERSION_TYPE
+IsAlternateVersion = CuraBuildType.lower() not in [DEFAULT_CURA_BUILD_TYPE, __ENTERPRISE_VERSION_TYPE]
+# NOTE: IsAlternateVersion is to make it possibile to have 'non-numbered' versions, at least as presented to the user.
+#       (Internally, it'll still have some sort of version-number, but the user is never meant to see it in the GUI).
+#       Warning: This will also change (some of) the icons/splash-screen to the 'work in progress' alternatives!
 
 try:
     from cura.CuraVersion import CuraAppDisplayName  # type: ignore

+ 2 - 2
cura/Arranging/Nest2DArrange.py

@@ -91,7 +91,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV
 
         if hull_polygon is not None and hull_polygon.getPoints() is not None and len(hull_polygon.getPoints()) > 2:  # numpy array has to be explicitly checked against None
             for point in hull_polygon.getPoints():
-                converted_points.append(Point(point[0] * factor, point[1] * factor))
+                converted_points.append(Point(int(point[0] * factor), int(point[1] * factor)))
             item = Item(converted_points)
             item.markAsFixedInBin(0)
             node_items.append(item)
@@ -159,4 +159,4 @@ def arrange(nodes_to_arrange: List["SceneNode"],
 
     grouped_operation, not_fit_count = createGroupOperationForArrange(nodes_to_arrange, build_volume, fixed_nodes, factor, add_new_nodes_in_scene)
     grouped_operation.push()
-    return not_fit_count != 0
+    return not_fit_count == 0

+ 5 - 2
cura/Backups/Backup.py

@@ -181,8 +181,7 @@ class Backup:
 
         return extracted
 
-    @staticmethod
-    def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
+    def _extractArchive(self, archive: "ZipFile", target_path: str) -> bool:
         """Extract the whole archive to the given target path.
 
         :param archive: The archive as ZipFile.
@@ -201,7 +200,11 @@ class Backup:
         Resources.factoryReset()
         Logger.log("d", "Extracting backup to location: %s", target_path)
         name_list = archive.namelist()
+        ignore_string = re.compile("|".join(self.IGNORED_FILES + self.IGNORED_FOLDERS))
         for archive_filename in name_list:
+            if ignore_string.search(archive_filename):
+                Logger.warning(f"File ({archive_filename}) in archive that doesn't fit current backup policy; ignored.")
+                continue
             try:
                 archive.extract(archive_filename, target_path)
             except (PermissionError, EnvironmentError):

+ 47 - 19
cura/BuildVolume.py

@@ -6,6 +6,7 @@ import math
 
 from typing import List, Optional, TYPE_CHECKING, Any, Set, cast, Iterable, Dict
 
+from UM.Logger import Logger
 from UM.Mesh.MeshData import MeshData
 from UM.Mesh.MeshBuilder import MeshBuilder
 
@@ -65,6 +66,7 @@ class BuildVolume(SceneNode):
         self._height = 0  # type: float
         self._depth = 0  # type: float
         self._shape = ""  # type: str
+        self._scale_vector = Vector(1.0, 1.0, 1.0)
 
         self._shader = None
 
@@ -289,7 +291,7 @@ class BuildVolume(SceneNode):
                 # Mark the node as outside build volume if the set extruder is disabled
                 extruder_position = node.callDecoration("getActiveExtruderPosition")
                 try:
-                    if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled:
+                    if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled and not node.callDecoration("isGroup"):
                         node.setOutsideBuildArea(True)
                         continue
                 except IndexError:  # Happens when the extruder list is too short. We're not done building the printer in memory yet.
@@ -512,6 +514,13 @@ class BuildVolume(SceneNode):
             self._disallowed_area_size = max(size, self._disallowed_area_size)
         return mb.build()
 
+    def _updateScaleFactor(self) -> None:
+        if not self._global_container_stack:
+            return
+        scale_xy = 100.0 / max(100.0, self._global_container_stack.getProperty("material_shrinkage_percentage_xy", "value"))
+        scale_z  = 100.0 / max(100.0, self._global_container_stack.getProperty("material_shrinkage_percentage_z" , "value"))
+        self._scale_vector = Vector(scale_xy, scale_xy, scale_z)
+
     def rebuild(self) -> None:
         """Recalculates the build volume & disallowed areas."""
 
@@ -553,9 +562,12 @@ class BuildVolume(SceneNode):
 
         self._error_mesh = self._buildErrorMesh(min_w, max_w, min_h, max_h, min_d, max_d, disallowed_area_height)
 
+        self._updateScaleFactor()
+
         self._volume_aabb = AxisAlignedBox(
-            minimum = Vector(min_w, min_h - 1.0, min_d),
-            maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d))
+            minimum = Vector(min_w, min_h - 1.0, min_d).scale(self._scale_vector),
+            maximum = Vector(max_w, max_h - self._raft_thickness - self._extra_z_clearance, max_d).scale(self._scale_vector)
+        )
 
         bed_adhesion_size = self.getEdgeDisallowedSize()
 
@@ -563,15 +575,15 @@ class BuildVolume(SceneNode):
         # This is probably wrong in all other cases. TODO!
         # The +1 and -1 is added as there is always a bit of extra room required to work properly.
         scale_to_max_bounds = AxisAlignedBox(
-            minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1),
-            maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1)
+            minimum = Vector(min_w + bed_adhesion_size + 1, min_h, min_d + self._disallowed_area_size - bed_adhesion_size + 1).scale(self._scale_vector),
+            maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - self._disallowed_area_size + bed_adhesion_size - 1).scale(self._scale_vector)
         )
 
         self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds  # type: ignore
 
         self.updateNodeBoundaryCheck()
 
-    def getBoundingBox(self):
+    def getBoundingBox(self) -> Optional[AxisAlignedBox]:
         return self._volume_aabb
 
     def getRaftThickness(self) -> float:
@@ -632,18 +644,18 @@ class BuildVolume(SceneNode):
             for extruder in extruders:
                 extruder.propertyChanged.connect(self._onSettingPropertyChanged)
 
-            self._width = self._global_container_stack.getProperty("machine_width", "value")
+            self._width = self._global_container_stack.getProperty("machine_width", "value") * self._scale_vector.x
             machine_height = self._global_container_stack.getProperty("machine_height", "value")
             if self._global_container_stack.getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
-                self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
-                if self._height < machine_height:
+                self._height = min(self._global_container_stack.getProperty("gantry_height", "value") * self._scale_vector.z, machine_height)
+                if self._height < (machine_height * self._scale_vector.z):
                     self._build_volume_message.show()
                 else:
                     self._build_volume_message.hide()
             else:
                 self._height = self._global_container_stack.getProperty("machine_height", "value")
                 self._build_volume_message.hide()
-            self._depth = self._global_container_stack.getProperty("machine_depth", "value")
+            self._depth = self._global_container_stack.getProperty("machine_depth", "value") * self._scale_vector.y
             self._shape = self._global_container_stack.getProperty("machine_shape", "value")
 
             self._updateDisallowedAreas()
@@ -677,18 +689,18 @@ class BuildVolume(SceneNode):
             if setting_key == "print_sequence":
                 machine_height = self._global_container_stack.getProperty("machine_height", "value")
                 if self._application.getGlobalContainerStack().getProperty("print_sequence", "value") == "one_at_a_time" and len(self._scene_objects) > 1:
-                    self._height = min(self._global_container_stack.getProperty("gantry_height", "value"), machine_height)
-                    if self._height < machine_height:
+                    self._height = min(self._global_container_stack.getProperty("gantry_height", "value") * self._scale_vector.z, machine_height)
+                    if self._height < (machine_height * self._scale_vector.z):
                         self._build_volume_message.show()
                     else:
                         self._build_volume_message.hide()
                 else:
-                    self._height = self._global_container_stack.getProperty("machine_height", "value")
+                    self._height = self._global_container_stack.getProperty("machine_height", "value") * self._scale_vector.z
                     self._build_volume_message.hide()
                 update_disallowed_areas = True
 
             # sometimes the machine size or shape settings are adjusted on the active machine, we should reflect this
-            if setting_key in self._machine_settings:
+            if setting_key in self._machine_settings or setting_key in self._material_size_settings:
                 self._updateMachineSizeProperties()
                 update_extra_z_clearance = True
                 update_disallowed_areas = True
@@ -737,9 +749,10 @@ class BuildVolume(SceneNode):
     def _updateMachineSizeProperties(self) -> None:
         if not self._global_container_stack:
             return
-        self._height = self._global_container_stack.getProperty("machine_height", "value")
-        self._width = self._global_container_stack.getProperty("machine_width", "value")
-        self._depth = self._global_container_stack.getProperty("machine_depth", "value")
+        self._updateScaleFactor()
+        self._height = self._global_container_stack.getProperty("machine_height", "value") * self._scale_vector.z
+        self._width = self._global_container_stack.getProperty("machine_width", "value") * self._scale_vector.x
+        self._depth = self._global_container_stack.getProperty("machine_depth", "value") * self._scale_vector.y
         self._shape = self._global_container_stack.getProperty("machine_shape", "value")
 
     def _updateDisallowedAreasAndRebuild(self):
@@ -756,6 +769,14 @@ class BuildVolume(SceneNode):
         self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks())
         self.rebuild()
 
+    def _scaleAreas(self, result_areas: List[Polygon]) -> None:
+        if self._global_container_stack is None:
+            return
+        for i, polygon in enumerate(result_areas):
+            result_areas[i] = polygon.scale(
+                100.0 / max(100.0, self._global_container_stack.getProperty("material_shrinkage_percentage_xy", "value"))
+            )
+
     def _updateDisallowedAreas(self) -> None:
         if not self._global_container_stack:
             return
@@ -811,9 +832,11 @@ class BuildVolume(SceneNode):
 
         self._disallowed_areas = []
         for extruder_id in result_areas:
+            self._scaleAreas(result_areas[extruder_id])
             self._disallowed_areas.extend(result_areas[extruder_id])
         self._disallowed_areas_no_brim = []
         for extruder_id in result_areas_no_brim:
+            self._scaleAreas(result_areas_no_brim[extruder_id])
             self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id])
 
     def _computeDisallowedAreasPrinted(self, used_extruders):
@@ -1078,7 +1101,11 @@ class BuildVolume(SceneNode):
         # setting does *not* have a limit_to_extruder setting (which means that we can't ask the global extruder what
         # the value is.
         adhesion_extruder = self._global_container_stack.getProperty("adhesion_extruder_nr", "value")
-        adhesion_stack = self._global_container_stack.extruderList[int(adhesion_extruder)]
+        try:
+            adhesion_stack = self._global_container_stack.extruderList[int(adhesion_extruder)]
+        except IndexError:
+            Logger.warning(f"Couldn't find extruder with index '{adhesion_extruder}', defaulting to 0 instead.")
+            adhesion_stack = self._global_container_stack.extruderList[0]
         skirt_brim_line_width = adhesion_stack.getProperty("skirt_brim_line_width", "value")
 
         initial_layer_line_width_factor = adhesion_stack.getProperty("initial_layer_line_width_factor", "value")
@@ -1195,4 +1222,5 @@ class BuildVolume(SceneNode):
     _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports", "wall_line_count", "wall_line_width_0", "wall_line_width_x"]
     _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
     _limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]
-    _disallowed_area_settings = _skirt_settings + _prime_settings + _tower_settings + _ooze_shield_settings + _distance_settings + _extruder_settings
+    _material_size_settings = ["material_shrinkage_percentage", "material_shrinkage_percentage_xy", "material_shrinkage_percentage_z"]
+    _disallowed_area_settings = _skirt_settings + _prime_settings + _tower_settings + _ooze_shield_settings + _distance_settings + _extruder_settings + _material_size_settings

+ 10 - 1
cura/CrashHandler.py

@@ -12,10 +12,12 @@ import json
 import locale
 from typing import cast, Any
 
+import sentry_sdk
+
 try:
     from sentry_sdk.hub import Hub
     from sentry_sdk.utils import event_from_exception
-    from sentry_sdk import configure_scope
+    from sentry_sdk import configure_scope, add_breadcrumb
     with_sentry_sdk = True
 except ImportError:
     with_sentry_sdk = False
@@ -424,6 +426,13 @@ class CrashHandler:
         if with_sentry_sdk:
             try:
                 hub = Hub.current
+                if not Logger.getLoggers():
+                    # No loggers have been loaded yet, so we don't have any breadcrumbs :(
+                    # So add them manually so we at least have some info...
+                    add_breadcrumb(level = "info", message = "SentryLogging was not initialised yet")
+                    for log_type, line in Logger.getUnloggedLines():
+                        add_breadcrumb(message=line)
+
                 event, hint = event_from_exception((self.exception_type, self.value, self.traceback))
                 hub.capture_event(event, hint=hint)
                 hub.flush()

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