Browse Source

Merge branch 'master' of github.com:Ultimaker/Cura into replace_controls_1_for_controls_2

Jaime van Kessel 3 years ago
parent
commit
f11d728c6b

+ 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

+ 27 - 22
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"""
 
@@ -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

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

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

+ 40 - 17
cura/BuildVolume.py

@@ -66,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
 
@@ -513,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."""
 
@@ -554,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()
 
@@ -564,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:
@@ -633,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()
@@ -678,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
@@ -738,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):
@@ -757,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
@@ -812,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):
@@ -1200,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

+ 3 - 3
cura/CuraApplication.py

@@ -152,11 +152,11 @@ class CuraApplication(QtApplication):
     def __init__(self, *args, **kwargs):
         super().__init__(name = ApplicationMetadata.CuraAppName,
                          app_display_name = ApplicationMetadata.CuraAppDisplayName,
-                         version = ApplicationMetadata.CuraVersion,
+                         version = ApplicationMetadata.CuraVersion if not ApplicationMetadata.IsAlternateVersion else ApplicationMetadata.CuraBuildType,
                          api_version = ApplicationMetadata.CuraSDKVersion,
                          build_type = ApplicationMetadata.CuraBuildType,
                          is_debug_mode = ApplicationMetadata.CuraDebugMode,
-                         tray_icon_name = "cura-icon-32.png",
+                         tray_icon_name = "cura-icon-32.png" if not ApplicationMetadata.IsAlternateVersion else "cura-icon-32_wip.png",
                          **kwargs)
 
         self.default_theme = "cura-light"
@@ -484,7 +484,7 @@ class CuraApplication(QtApplication):
 
         if not self.getIsHeadLess():
             try:
-                self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
+                self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png" if not ApplicationMetadata.IsAlternateVersion else "cura-icon_wip.png")))
             except FileNotFoundError:
                 Logger.log("w", "Unable to find the window icon.")
 

+ 30 - 4
cura/Machines/Models/GlobalStacksModel.py

@@ -2,7 +2,7 @@
 # Cura is released under the terms of the LGPLv3 or higher.
 
 from PyQt5.QtCore import Qt, QTimer, pyqtProperty, pyqtSignal
-from typing import Optional
+from typing import List, Optional
 
 from UM.Qt.ListModel import ListModel
 from UM.i18n import i18nCatalog
@@ -11,6 +11,7 @@ from UM.Util import parseBool
 from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
 from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
 from cura.Settings.GlobalStack import GlobalStack
+from cura.UltimakerCloud.UltimakerCloudConstants import META_CAPABILITIES  # To filter on the printer's capabilities.
 
 
 class GlobalStacksModel(ListModel):
@@ -42,6 +43,7 @@ class GlobalStacksModel(ListModel):
 
         self._filter_connection_type = None  # type: Optional[ConnectionType]
         self._filter_online_only = False
+        self._filter_capabilities: List[str] = []  # Required capabilities that all listed printers must have.
 
         # Listen to changes
         CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
@@ -50,8 +52,13 @@ class GlobalStacksModel(ListModel):
         self._updateDelayed()
 
     filterConnectionTypeChanged = pyqtSignal()
+    filterCapabilitiesChanged = pyqtSignal()
+    filterOnlineOnlyChanged = pyqtSignal()
+
     def setFilterConnectionType(self, new_filter: Optional[ConnectionType]) -> None:
-        self._filter_connection_type = new_filter
+        if self._filter_connection_type != new_filter:
+            self._filter_connection_type = new_filter
+            self.filterConnectionTypeChanged.emit()
 
     @pyqtProperty(int, fset = setFilterConnectionType, notify = filterConnectionTypeChanged)
     def filterConnectionType(self) -> int:
@@ -65,9 +72,10 @@ class GlobalStacksModel(ListModel):
             return -1
         return self._filter_connection_type.value
 
-    filterOnlineOnlyChanged = pyqtSignal()
     def setFilterOnlineOnly(self, new_filter: bool) -> None:
-        self._filter_online_only = new_filter
+        if self._filter_online_only != new_filter:
+            self._filter_online_only = new_filter
+            self.filterOnlineOnlyChanged.emit()
 
     @pyqtProperty(bool, fset = setFilterOnlineOnly, notify = filterOnlineOnlyChanged)
     def filterOnlineOnly(self) -> bool:
@@ -76,6 +84,20 @@ class GlobalStacksModel(ListModel):
         """
         return self._filter_online_only
 
+    def setFilterCapabilities(self, new_filter: List[str]) -> None:
+        if self._filter_capabilities != new_filter:
+            self._filter_capabilities = new_filter
+            self.filterCapabilitiesChanged.emit()
+
+    @pyqtProperty("QStringList", fset = setFilterCapabilities, notify = filterCapabilitiesChanged)
+    def filterCapabilities(self) -> List[str]:
+        """
+        Capabilities to require on the list of printers.
+
+        Only printers that have all of these capabilities will be shown in this model.
+        """
+        return self._filter_capabilities
+
     def _onContainerChanged(self, container) -> None:
         """Handler for container added/removed events from registry"""
 
@@ -108,6 +130,10 @@ class GlobalStacksModel(ListModel):
             if self._filter_online_only and not is_online:
                 continue
 
+            capabilities = set(container_stack.getMetaDataEntry(META_CAPABILITIES, "").split(","))
+            if set(self._filter_capabilities) - capabilities:  # Not all required capabilities are met.
+                continue
+
             device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName())
             section_name = "Connected printers" if has_remote_connection else "Preset printers"
             section_name = self._catalog.i18nc("@info:title", section_name)

+ 5 - 1
cura/Machines/Models/IntentCategoryModel.py

@@ -106,11 +106,15 @@ class IntentCategoryModel(ListModel):
         for category in available_categories:
             qualities = IntentModel()
             qualities.setIntentCategory(category)
+            try:
+                weight = list(IntentCategoryModel._get_translations().keys()).index(category)
+            except ValueError:
+                weight = 99
             result.append({
                 "name": IntentCategoryModel.translation(category, "name", category),
                 "description": IntentCategoryModel.translation(category, "description", None),
                 "intent_category": category,
-                "weight": list(IntentCategoryModel._get_translations().keys()).index(category),
+                "weight": weight,
                 "qualities": qualities
             })
         result.sort(key = lambda k: k["weight"])

+ 8 - 1
cura/Machines/Models/QualityManagementModel.py

@@ -361,8 +361,15 @@ class QualityManagementModel(ListModel):
                 "section_name": catalog.i18nc("@label", intent_translations.get(intent_category, {}).get("name", catalog.i18nc("@label", "Unknown"))),
             })
         # Sort by quality_type for each intent category
+        intent_translations_list = list(intent_translations)
 
-        result = sorted(result, key = lambda x: (list(intent_translations).index(x["intent_category"]), x["quality_type"]))
+        def getIntentWeight(intent_category):
+            try:
+                return intent_translations_list.index(intent_category)
+            except ValueError:
+                return 99
+
+        result = sorted(result, key = lambda x: (getIntentWeight(x["intent_category"]), x["quality_type"]))
         item_list += result
 
         # Create quality_changes group items

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