diff --git a/.github/no-response.yml b/.github/no-response.yml new file mode 100644 index 0000000000..a386aaa7ba --- /dev/null +++ b/.github/no-response.yml @@ -0,0 +1,13 @@ +# Configuration for probot-no-response - https://github.com/probot/no-response + +# Number of days of inactivity before an Issue is closed for lack of response +daysUntilClose: 14 +# Label requiring a response +responseRequiredLabel: 'Status: Needs Info' +# Comment to post when closing an Issue for lack of response. Set to `false` to disable +closeComment: > + This issue has been automatically closed because there has been no response + to our request for more information from the original author. With only the + information that is currently in the issue, we don't have enough information + to take action. Please reach out if you have or find the answers we need so + that we can investigate further. diff --git a/README.md b/README.md index 93abcc0c61..1ba2b3c277 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ Cura ==== -This is the new, shiny frontend for Cura. Check [daid/LegacyCura](https://github.com/daid/LegacyCura) for the legacy Cura that everyone knows and loves/hates. We re-worked the whole GUI code at Ultimaker, because the old code started to become unmaintainable. +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) Logging Issues ------------ diff --git a/cmake/mod_bundled_packages_json.py b/cmake/mod_bundled_packages_json.py index 6423591f57..e03261b479 100755 --- a/cmake/mod_bundled_packages_json.py +++ b/cmake/mod_bundled_packages_json.py @@ -11,11 +11,13 @@ import os import sys -## Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths. -# -# \param work_dir The directory to look for JSON files recursively. -# \return A list of JSON files in absolute paths that are found in the given directory. def find_json_files(work_dir: str) -> list: + """Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths. + + :param work_dir: The directory to look for JSON files recursively. + :return: A list of JSON files in absolute paths that are found in the given directory. + """ + json_file_list = [] for root, dir_names, file_names in os.walk(work_dir): for file_name in file_names: @@ -24,12 +26,14 @@ def find_json_files(work_dir: str) -> list: return json_file_list -## Removes the given entries from the given JSON file. The file will modified in-place. -# -# \param file_path The JSON file to modify. -# \param entries A list of strings as entries to remove. -# \return None def remove_entries_from_json_file(file_path: str, entries: list) -> None: + """Removes the given entries from the given JSON file. The file will modified in-place. + + :param file_path: The JSON file to modify. + :param entries: A list of strings as entries to remove. + :return: None + """ + try: with open(file_path, "r", encoding = "utf-8") as f: package_dict = json.load(f, object_hook = collections.OrderedDict) diff --git a/cura/API/Account.py b/cura/API/Account.py index 7e8802eddd..e190fe9b42 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -10,7 +10,7 @@ from UM.Message import Message from UM.i18n import i18nCatalog from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.Models import OAuth2Settings -from cura.UltimakerCloud import UltimakerCloudAuthentication +from cura.UltimakerCloud import UltimakerCloudConstants if TYPE_CHECKING: from cura.CuraApplication import CuraApplication @@ -23,24 +23,29 @@ class SyncState: SYNCING = 0 SUCCESS = 1 ERROR = 2 + IDLE = 3 - -## The account API provides a version-proof bridge to use Ultimaker Accounts -# -# Usage: -# ``from cura.API import CuraAPI -# api = CuraAPI() -# api.account.login() -# api.account.logout() -# api.account.userProfile # Who is logged in`` -# class Account(QObject): + """The account API provides a version-proof bridge to use Ultimaker Accounts + + Usage: + + .. code-block:: python + + from cura.API import CuraAPI + api = CuraAPI() + api.account.login() + api.account.logout() + api.account.userProfile # Who is logged in + """ + # The interval in which sync services are automatically triggered SYNC_INTERVAL = 30.0 # seconds Q_ENUMS(SyncState) - # Signal emitted when user logged in or out. loginStateChanged = pyqtSignal(bool) + """Signal emitted when user logged in or out""" + accessTokenChanged = pyqtSignal() syncRequested = pyqtSignal() """Sync services may connect to this signal to receive sync triggers. @@ -50,6 +55,7 @@ class Account(QObject): """ lastSyncDateTimeChanged = pyqtSignal() syncStateChanged = pyqtSignal(int) # because SyncState is an int Enum + manualSyncEnabledChanged = pyqtSignal(bool) def __init__(self, application: "CuraApplication", parent = None) -> None: super().__init__(parent) @@ -58,11 +64,12 @@ class Account(QObject): self._error_message = None # type: Optional[Message] self._logged_in = False - self._sync_state = SyncState.SUCCESS + self._sync_state = SyncState.IDLE + self._manual_sync_enabled = False self._last_sync_str = "-" self._callback_port = 32118 - self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot + self._oauth_root = UltimakerCloudConstants.CuraCloudAccountAPIRoot self._oauth_settings = OAuth2Settings( OAUTH_SERVER_URL= self._oauth_root, @@ -96,6 +103,11 @@ class Account(QObject): self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged) self._authorization_service.loadAuthDataFromPreferences() + + @pyqtProperty(int, notify=syncStateChanged) + def syncState(self): + return self._sync_state + def setSyncState(self, service_name: str, state: int) -> None: """ Can be used to register sync services and update account sync states @@ -105,17 +117,19 @@ class Account(QObject): :param service_name: A unique name for your service, such as `plugins` or `backups` :param state: One of SyncState """ - prev_state = self._sync_state self._sync_services[service_name] = state if any(val == SyncState.SYNCING for val in self._sync_services.values()): self._sync_state = SyncState.SYNCING + self._setManualSyncEnabled(False) elif any(val == SyncState.ERROR for val in self._sync_services.values()): self._sync_state = SyncState.ERROR + self._setManualSyncEnabled(True) else: self._sync_state = SyncState.SUCCESS + self._setManualSyncEnabled(False) if self._sync_state != prev_state: self.syncStateChanged.emit(self._sync_state) @@ -132,9 +146,10 @@ class Account(QObject): def _onAccessTokenChanged(self): self.accessTokenChanged.emit() - ## Returns a boolean indicating whether the given authentication is applied against staging or not. @property def is_staging(self) -> bool: + """Indication whether the given authentication is applied against staging or not.""" + return "staging" in self._oauth_root @pyqtProperty(bool, notify=loginStateChanged) @@ -157,11 +172,31 @@ class Account(QObject): self._logged_in = logged_in self.loginStateChanged.emit(logged_in) if logged_in: - self.sync() + self._setManualSyncEnabled(False) + self._sync() else: if self._update_timer.isActive(): self._update_timer.stop() + def _sync(self) -> None: + """Signals all sync services to start syncing + + This can be considered a forced sync: even when a + sync is currently running, a sync will be requested. + """ + + if self._update_timer.isActive(): + self._update_timer.stop() + elif self._sync_state == SyncState.SYNCING: + Logger.warning("Starting a new sync while previous sync was not completed\n{}", str(self._sync_services)) + + self.syncRequested.emit() + + def _setManualSyncEnabled(self, enabled: bool) -> None: + if self._manual_sync_enabled != enabled: + self._manual_sync_enabled = enabled + self.manualSyncEnabledChanged.emit(enabled) + @pyqtSlot() @pyqtSlot(bool) def login(self, force_logout_before_login: bool = False) -> None: @@ -199,10 +234,10 @@ class Account(QObject): def accessToken(self) -> Optional[str]: return self._authorization_service.getAccessToken() - # Get the profile of the logged in user - # @returns None if no user is logged in, a dict containing user_id, username and profile_image_url @pyqtProperty("QVariantMap", notify = loginStateChanged) 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: return None @@ -212,20 +247,23 @@ class Account(QObject): def lastSyncDateTime(self) -> str: return self._last_sync_str + @pyqtProperty(bool, notify=manualSyncEnabledChanged) + def manualSyncEnabled(self) -> bool: + return self._manual_sync_enabled + @pyqtSlot() - def sync(self) -> None: - """Signals all sync services to start syncing + @pyqtSlot(bool) + def sync(self, user_initiated: bool = False) -> None: + if user_initiated: + self._setManualSyncEnabled(False) - This can be considered a forced sync: even when a - sync is currently running, a sync will be requested. - """ + self._sync() - if self._update_timer.isActive(): - self._update_timer.stop() - elif self._sync_state == SyncState.SYNCING: - Logger.warning("Starting a new sync while previous sync was not completed\n{}", str(self._sync_services)) - - self.syncRequested.emit() + @pyqtSlot() + def popupOpened(self) -> None: + self._setManualSyncEnabled(True) + self._sync_state = SyncState.IDLE + self.syncStateChanged.emit(self._sync_state) @pyqtSlot() def logout(self) -> None: diff --git a/cura/API/Backups.py b/cura/API/Backups.py index ef74e74be0..1940d38a36 100644 --- a/cura/API/Backups.py +++ b/cura/API/Backups.py @@ -8,28 +8,37 @@ if TYPE_CHECKING: from cura.CuraApplication import CuraApplication -## The back-ups API provides a version-proof bridge between Cura's -# BackupManager and plug-ins that hook into it. -# -# Usage: -# ``from cura.API import CuraAPI -# api = CuraAPI() -# api.backups.createBackup() -# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})`` class Backups: + """The back-ups API provides a version-proof bridge between Cura's + + BackupManager and plug-ins that hook into it. + + Usage: + + .. code-block:: python + + from cura.API import CuraAPI + api = CuraAPI() + api.backups.createBackup() + api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) + """ def __init__(self, application: "CuraApplication") -> None: self.manager = BackupsManager(application) - ## Create a new back-up using the BackupsManager. - # \return Tuple containing a ZIP file with the back-up data and a dict - # with metadata about the back-up. def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]: + """Create a new back-up using the BackupsManager. + + :return: Tuple containing a ZIP file with the back-up data and a dict with metadata about the back-up. + """ + return self.manager.createBackup() - ## Restore a back-up using the BackupsManager. - # \param zip_file A ZIP file containing the actual back-up data. - # \param meta_data Some metadata needed for restoring a back-up, like the - # Cura version number. def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None: + """Restore a back-up using the BackupsManager. + + :param zip_file: A ZIP file containing the actual back-up data. + :param meta_data: Some metadata needed for restoring a back-up, like the Cura version number. + """ + return self.manager.restoreBackup(zip_file, meta_data) diff --git a/cura/API/ConnectionStatus.py b/cura/API/ConnectionStatus.py new file mode 100644 index 0000000000..36f804e3cf --- /dev/null +++ b/cura/API/ConnectionStatus.py @@ -0,0 +1,41 @@ +from typing import Optional + +from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty + +from UM.TaskManagement.HttpRequestManager import HttpRequestManager + + +class ConnectionStatus(QObject): + """Provides an estimation of whether internet is reachable + + Estimation is updated with every request through HttpRequestManager. + Acts as a proxy to HttpRequestManager.internetReachableChanged without + exposing the HttpRequestManager in its entirety. + """ + + __instance = None # type: Optional[ConnectionStatus] + + internetReachableChanged = pyqtSignal() + + @classmethod + def getInstance(cls, *args, **kwargs) -> "ConnectionStatus": + if cls.__instance is None: + cls.__instance = cls(*args, **kwargs) + return cls.__instance + + def __init__(self, parent: Optional["QObject"] = None) -> None: + super().__init__(parent) + + manager = HttpRequestManager.getInstance() + self._is_internet_reachable = manager.isInternetReachable # type: bool + manager.internetReachableChanged.connect(self._onInternetReachableChanged) + + @pyqtProperty(bool, notify = internetReachableChanged) + def isInternetReachable(self) -> bool: + return self._is_internet_reachable + + def _onInternetReachableChanged(self, reachable: bool): + if reachable != self._is_internet_reachable: + self._is_internet_reachable = reachable + self.internetReachableChanged.emit() + diff --git a/cura/API/Interface/Settings.py b/cura/API/Interface/Settings.py index 371c40c14c..706a6d8c74 100644 --- a/cura/API/Interface/Settings.py +++ b/cura/API/Interface/Settings.py @@ -7,32 +7,43 @@ if TYPE_CHECKING: from cura.CuraApplication import CuraApplication -## The Interface.Settings API provides a version-proof bridge between Cura's -# (currently) sidebar UI and plug-ins that hook into it. -# -# Usage: -# ``from cura.API import CuraAPI -# api = CuraAPI() -# api.interface.settings.getContextMenuItems() -# data = { -# "name": "My Plugin Action", -# "iconName": "my-plugin-icon", -# "actions": my_menu_actions, -# "menu_item": MyPluginAction(self) -# } -# api.interface.settings.addContextMenuItem(data)`` - class Settings: + """The Interface.Settings API provides a version-proof bridge + between Cura's + + (currently) sidebar UI and plug-ins that hook into it. + + Usage: + + .. code-block:: python + + from cura.API import CuraAPI + api = CuraAPI() + api.interface.settings.getContextMenuItems() + data = { + "name": "My Plugin Action", + "iconName": "my-plugin-icon", + "actions": my_menu_actions, + "menu_item": MyPluginAction(self) + } + api.interface.settings.addContextMenuItem(data) + """ def __init__(self, application: "CuraApplication") -> None: self.application = application - ## Add items to the sidebar context menu. - # \param menu_item dict containing the menu item to add. def addContextMenuItem(self, menu_item: dict) -> None: + """Add items to the sidebar context menu. + + :param menu_item: dict containing the menu item to add. + """ + self.application.addSidebarCustomMenuItem(menu_item) - ## Get all custom items currently added to the sidebar context menu. - # \return List containing all custom context menu items. def getContextMenuItems(self) -> list: + """Get all custom items currently added to the sidebar context menu. + + :return: List containing all custom context menu items. + """ + return self.application.getSidebarCustomMenuItems() diff --git a/cura/API/Interface/__init__.py b/cura/API/Interface/__init__.py index cec174bf0a..61510d6262 100644 --- a/cura/API/Interface/__init__.py +++ b/cura/API/Interface/__init__.py @@ -9,18 +9,22 @@ if TYPE_CHECKING: from cura.CuraApplication import CuraApplication -## The Interface class serves as a common root for the specific API -# methods for each interface element. -# -# Usage: -# ``from cura.API import CuraAPI -# api = CuraAPI() -# api.interface.settings.addContextMenuItem() -# api.interface.viewport.addOverlay() # Not implemented, just a hypothetical -# api.interface.toolbar.getToolButtonCount() # Not implemented, just a hypothetical -# # etc.`` - class Interface: + """The Interface class serves as a common root for the specific API + + methods for each interface element. + + Usage: + + .. code-block:: python + + from cura.API import CuraAPI + api = CuraAPI() + api.interface.settings.addContextMenuItem() + api.interface.viewport.addOverlay() # Not implemented, just a hypothetical + api.interface.toolbar.getToolButtonCount() # Not implemented, just a hypothetical + # etc + """ def __init__(self, application: "CuraApplication") -> None: # API methods specific to the settings portion of the UI diff --git a/cura/API/__init__.py b/cura/API/__init__.py index 26c9a4c829..447be98e4b 100644 --- a/cura/API/__init__.py +++ b/cura/API/__init__.py @@ -5,6 +5,7 @@ from typing import Optional, TYPE_CHECKING from PyQt5.QtCore import QObject, pyqtProperty from cura.API.Backups import Backups +from cura.API.ConnectionStatus import ConnectionStatus from cura.API.Interface import Interface from cura.API.Account import Account @@ -12,13 +13,14 @@ if TYPE_CHECKING: from cura.CuraApplication import CuraApplication -## The official Cura API that plug-ins can use to interact with Cura. -# -# Python does not technically prevent talking to other classes as well, but -# this API provides a version-safe interface with proper deprecation warnings -# etc. Usage of any other methods than the ones provided in this API can cause -# plug-ins to be unstable. class CuraAPI(QObject): + """The official Cura API that plug-ins can use to interact with Cura. + + Python does not technically prevent talking to other classes as well, but this API provides a version-safe + interface with proper deprecation warnings etc. Usage of any other methods than the ones provided in this API can + cause plug-ins to be unstable. + """ + # For now we use the same API version to be consistent. __instance = None # type: "CuraAPI" @@ -39,12 +41,12 @@ class CuraAPI(QObject): def __init__(self, application: Optional["CuraApplication"] = None) -> None: super().__init__(parent = CuraAPI._application) - # Accounts API self._account = Account(self._application) - # Backups API self._backups = Backups(self._application) + self._connectionStatus = ConnectionStatus() + # Interface API self._interface = Interface(self._application) @@ -53,12 +55,22 @@ class CuraAPI(QObject): @pyqtProperty(QObject, constant = True) def account(self) -> "Account": + """Accounts API""" + return self._account + @pyqtProperty(QObject, constant = True) + def connectionStatus(self) -> "ConnectionStatus": + return self._connectionStatus + @property def backups(self) -> "Backups": + """Backups API""" + return self._backups @property def interface(self) -> "Interface": + """Interface API""" + return self._interface diff --git a/cura/Arranging/Arrange.py b/cura/Arranging/Arrange.py index 35f155f4cf..c9d3498c7b 100644 --- a/cura/Arranging/Arrange.py +++ b/cura/Arranging/Arrange.py @@ -16,17 +16,20 @@ from collections import namedtuple import numpy import copy -## Return object for bestSpot LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"]) +"""Return object for bestSpot""" class Arrange: """ - The Arrange classed is used together with ShapeArray. Use it to find good locations for objects that you try to put + The Arrange classed is used together with :py:class:`cura.Arranging.ShapeArray.ShapeArray`. Use it to find good locations for objects that you try to put on a build place. Different priority schemes can be defined so it alters the behavior while using the same logic. - Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance. + .. note:: + + Make sure the scale is the same between :py:class:`cura.Arranging.ShapeArray.ShapeArray` objects and the :py:class:`cura.Arranging.Arrange.Arrange` instance. """ + build_volume = None # type: Optional[BuildVolume] def __init__(self, x, y, offset_x, offset_y, scale = 0.5): @@ -42,20 +45,20 @@ class Arrange: self._is_empty = True @classmethod - def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8): - """ - Helper to create an Arranger instance + def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8) -> "Arrange": + """Helper to create an :py:class:`cura.Arranging.Arrange.Arrange` instance Either fill in scene_root and create will find all sliceable nodes by itself, or use fixed_nodes to provide the nodes yourself. - :param scene_root: Root for finding all scene nodes - :param fixed_nodes: Scene nodes to be placed - :param scale: - :param x: - :param y: - :param min_offset: - :return: + + :param scene_root: Root for finding all scene nodes default = None + :param fixed_nodes: Scene nodes to be placed default = None + :param scale: default = 0.5 + :param x: default = 350 + :param y: default = 250 + :param min_offset: default = 8 """ + arranger = Arrange(x, y, x // 2, y // 2, scale = scale) arranger.centerFirst() @@ -77,8 +80,11 @@ class Arrange: # After scaling (like up to 0.1 mm) the node might not have points if not points.size: continue - - shape_arr = ShapeArray.fromPolygon(points, scale = scale) + try: + shape_arr = ShapeArray.fromPolygon(points, scale = scale) + except ValueError: + Logger.logException("w", "Unable to create polygon") + continue arranger.place(0, 0, shape_arr) # If a build volume was set, add the disallowed areas @@ -90,19 +96,21 @@ class Arrange: arranger.place(0, 0, shape_arr, update_empty = False) return arranger - ## This resets the optimization for finding location based on size def resetLastPriority(self): + """This resets the optimization for finding location based on size""" + self._last_priority = 0 - def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1): - """ - Find placement for a node (using offset shape) and place it (using hull shape) - :param node: - :param offset_shape_arr: hapeArray with offset, for placing the shape - :param hull_shape_arr: ShapeArray without offset, used to find location - :param step: + def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1) -> bool: + """Find placement for a node (using offset shape) and place it (using hull shape) + + :param node: The node to be placed + :param offset_shape_arr: shape array with offset, for placing the shape + :param hull_shape_arr: shape array without offset, used to find location + :param step: default = 1 :return: the nodes that should be placed """ + best_spot = self.bestSpot( hull_shape_arr, start_prio = self._last_priority, step = step) x, y = best_spot.x, best_spot.y @@ -129,10 +137,8 @@ class Arrange: return found_spot def centerFirst(self): - """ - Fill priority, center is best. Lower value is better. - :return: - """ + """Fill priority, center is best. Lower value is better. """ + # Square distance: creates a more round shape self._priority = numpy.fromfunction( lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32) @@ -140,23 +146,22 @@ class Arrange: self._priority_unique_values.sort() def backFirst(self): - """ - Fill priority, back is best. Lower value is better - :return: - """ + """Fill priority, back is best. Lower value is better """ + self._priority = numpy.fromfunction( lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32) self._priority_unique_values = numpy.unique(self._priority) self._priority_unique_values.sort() - def checkShape(self, x, y, shape_arr): - """ - Return the amount of "penalty points" for polygon, which is the sum of priority + def checkShape(self, x, y, shape_arr) -> Optional[numpy.ndarray]: + """Return the amount of "penalty points" for polygon, which is the sum of priority + :param x: x-coordinate to check shape - :param y: - :param shape_arr: the ShapeArray object to place + :param y: y-coordinate to check shape + :param shape_arr: the shape array object to place :return: None if occupied """ + x = int(self._scale * x) y = int(self._scale * y) offset_x = x + self._offset_x + shape_arr.offset_x @@ -180,14 +185,15 @@ class Arrange: offset_x:offset_x + shape_arr.arr.shape[1]] return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)]) - def bestSpot(self, shape_arr, start_prio = 0, step = 1): - """ - Find "best" spot for ShapeArray - :param shape_arr: + def bestSpot(self, shape_arr, start_prio = 0, step = 1) -> LocationSuggestion: + """Find "best" spot for ShapeArray + + :param shape_arr: shape array :param start_prio: Start with this priority value (and skip the ones before) :param step: Slicing value, higher = more skips = faster but less accurate :return: namedtuple with properties x, y, penalty_points, priority. """ + start_idx_list = numpy.where(self._priority_unique_values == start_prio) if start_idx_list: try: @@ -211,15 +217,16 @@ class Arrange: return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority) # No suitable location found :-( def place(self, x, y, shape_arr, update_empty = True): - """ - Place the object. + """Place the object. + Marks the locations in self._occupied and self._priority + :param x: :param y: :param shape_arr: :param update_empty: updates the _is_empty, used when adding disallowed areas - :return: """ + x = int(self._scale * x) y = int(self._scale * y) offset_x = x + self._offset_x + shape_arr.offset_x diff --git a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py index 7736efbeeb..0f337a229b 100644 --- a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py @@ -18,8 +18,9 @@ from cura.Arranging.ShapeArray import ShapeArray from typing import List -## Do arrangements on multiple build plates (aka builtiplexer) class ArrangeArray: + """Do arrangements on multiple build plates (aka builtiplexer)""" + def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]) -> None: self._x = x self._y = y diff --git a/cura/Arranging/ShapeArray.py b/cura/Arranging/ShapeArray.py index 403db5e706..840f9731c2 100644 --- a/cura/Arranging/ShapeArray.py +++ b/cura/Arranging/ShapeArray.py @@ -11,19 +11,24 @@ if TYPE_CHECKING: from UM.Scene.SceneNode import SceneNode -## Polygon representation as an array for use with Arrange class ShapeArray: + """Polygon representation as an array for use with :py:class:`cura.Arranging.Arrange.Arrange`""" + def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None: self.arr = arr self.offset_x = offset_x self.offset_y = offset_y self.scale = scale - ## Instantiate from a bunch of vertices - # \param vertices - # \param scale scale the coordinates @classmethod def fromPolygon(cls, vertices: numpy.array, scale: float = 1) -> "ShapeArray": + """Instantiate from a bunch of vertices + + :param vertices: + :param scale: scale the coordinates + :return: a shape array instantiated from a bunch of vertices + """ + # scale vertices = vertices * scale # flip y, x -> x, y @@ -44,12 +49,16 @@ class ShapeArray: arr[0][0] = 1 return cls(arr, offset_x, offset_y) - ## Instantiate an offset and hull ShapeArray from a scene node. - # \param node source node where the convex hull must be present - # \param min_offset offset for the offset ShapeArray - # \param scale scale the coordinates @classmethod def fromNode(cls, node: "SceneNode", min_offset: float, scale: float = 0.5, include_children: bool = False) -> Tuple[Optional["ShapeArray"], Optional["ShapeArray"]]: + """Instantiate an offset and hull ShapeArray from a scene node. + + :param node: source node where the convex hull must be present + :param min_offset: offset for the offset ShapeArray + :param scale: scale the coordinates + :return: A tuple containing an offset and hull shape array + """ + transform = node._transformation transform_x = transform._data[0][3] transform_y = transform._data[2][3] @@ -88,14 +97,19 @@ class ShapeArray: return offset_shape_arr, hull_shape_arr - ## Create np.array with dimensions defined by shape - # Fills polygon defined by vertices with ones, all other values zero - # Only works correctly for convex hull vertices - # Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array - # \param shape numpy format shape, [x-size, y-size] - # \param vertices @classmethod def arrayFromPolygon(cls, shape: Tuple[int, int], vertices: numpy.array) -> numpy.array: + """Create :py:class:`numpy.ndarray` with dimensions defined by shape + + Fills polygon defined by vertices with ones, all other values zero + Only works correctly for convex hull vertices + Originally from: `Stackoverflow - generating a filled polygon inside a numpy array `_ + + :param shape: numpy format shape, [x-size, y-size] + :param vertices: + :return: numpy array with dimensions defined by shape + """ + base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill @@ -111,16 +125,21 @@ class ShapeArray: return base_array - ## Return indices that mark one side of the line, used by arrayFromPolygon - # Uses the line defined by p1 and p2 to check array of - # input indices against interpolated value - # Returns boolean array, with True inside and False outside of shape - # Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array - # \param p1 2-tuple with x, y for point 1 - # \param p2 2-tuple with x, y for point 2 - # \param base_array boolean array to project the line on @classmethod def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> Optional[numpy.array]: + """Return indices that mark one side of the line, used by arrayFromPolygon + + Uses the line defined by p1 and p2 to check array of + input indices against interpolated value + Returns boolean array, with True inside and False outside of shape + Originally from: `Stackoverflow - generating a filled polygon inside a numpy array `_ + + :param p1: 2-tuple with x, y for point 1 + :param p2: 2-tuple with x, y for point 2 + :param base_array: boolean array to project the line on + :return: A numpy array with indices that mark one side of the line + """ + if p1[0] == p2[0] and p1[1] == p2[1]: return None idxs = numpy.indices(base_array.shape) # Create 3D array of indices diff --git a/cura/AutoSave.py b/cura/AutoSave.py index 2c1dbe4a84..d80e34771e 100644 --- a/cura/AutoSave.py +++ b/cura/AutoSave.py @@ -31,7 +31,6 @@ class AutoSave: self._change_timer.timeout.connect(self._onTimeout) self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged) self._onGlobalStackChanged() - self._triggerTimer() def _triggerTimer(self, *args: Any) -> None: if not self._saving: diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 4d24a46384..44e1feef30 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -18,24 +18,26 @@ if TYPE_CHECKING: from cura.CuraApplication import CuraApplication -## The back-up class holds all data about a back-up. -# -# It is also responsible for reading and writing the zip file to the user data -# folder. class Backup: - # These files should be ignored when making a backup. - IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"] + """The back-up class holds all data about a back-up. + + It is also responsible for reading and writing the zip file to the user data folder. + """ + + IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"] + """These files should be ignored when making a backup.""" - # Re-use translation catalog. catalog = i18nCatalog("cura") + """Re-use translation catalog""" def __init__(self, application: "CuraApplication", zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None: self._application = application self.zip_file = zip_file # type: Optional[bytes] self.meta_data = meta_data # type: Optional[Dict[str, str]] - ## Create a back-up from the current user config folder. def makeFromCurrent(self) -> None: + """Create a back-up from the current user config folder.""" + cura_release = self._application.getVersion() version_data_dir = Resources.getDataStoragePath() @@ -66,7 +68,7 @@ class Backup: material_count = len([s for s in files if "materials/" in s]) - 1 profile_count = len([s for s in files if "quality_changes/" in s]) - 1 plugin_count = len([s for s in files if "plugin.json" in s]) - + # Store the archive and metadata so the BackupManager can fetch them when needed. self.zip_file = buffer.getvalue() self.meta_data = { @@ -77,10 +79,13 @@ class Backup: "plugin_count": str(plugin_count) } - ## Make a full archive from the given root path with the given name. - # \param root_path The root directory to archive recursively. - # \return The archive as bytes. def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]: + """Make a full archive from the given root path with the given name. + + :param root_path: The root directory to archive recursively. + :return: The archive as bytes. + """ + ignore_string = re.compile("|".join(self.IGNORED_FILES)) try: archive = ZipFile(buffer, "w", ZIP_DEFLATED) @@ -99,13 +104,17 @@ class Backup: "Could not create archive from user data directory: {}".format(error))) return None - ## Show a UI message. def _showMessage(self, message: str) -> None: + """Show a UI message.""" + Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show() - ## Restore this back-up. - # \return Whether we had success or not. def restore(self) -> bool: + """Restore this back-up. + + :return: Whether we had success or not. + """ + if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): # We can restore without the minimum required information. Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") @@ -139,12 +148,14 @@ class Backup: return extracted - ## Extract the whole archive to the given target path. - # \param archive The archive as ZipFile. - # \param target_path The target path. - # \return Whether we had success or not. @staticmethod def _extractArchive(archive: "ZipFile", target_path: str) -> bool: + """Extract the whole archive to the given target path. + + :param archive: The archive as ZipFile. + :param target_path: The target path. + :return: Whether we had success or not. + """ # Implement security recommendations: Sanity check on zip files will make it harder to spoof. from cura.CuraApplication import CuraApplication diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index f335a3fb04..fb758455c1 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -24,6 +24,7 @@ class BackupsManager: def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]: """ Get a back-up of the current configuration. + :return: A tuple containing a ZipFile (the actual back-up) and a dict containing some metadata (like version). """ @@ -37,6 +38,7 @@ class BackupsManager: def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str]) -> None: """ Restore a back-up from a given ZipFile. + :param zip_file: A bytes object containing the actual back-up. :param meta_data: A dict containing some metadata that is needed to restore the back-up correctly. """ diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 36edd427f7..315eca9fe5 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -44,8 +44,9 @@ catalog = i18nCatalog("cura") PRIME_CLEARANCE = 6.5 -## Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas. class BuildVolume(SceneNode): + """Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas.""" + raftThicknessChanged = Signal() def __init__(self, application: "CuraApplication", parent: Optional[SceneNode] = None) -> None: @@ -113,7 +114,7 @@ class BuildVolume(SceneNode): self._has_errors = False self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged) - #Objects loaded at the moment. We are connected to the property changed events of these objects. + # Objects loaded at the moment. We are connected to the property changed events of these objects. self._scene_objects = set() # type: Set[SceneNode] self._scene_change_timer = QTimer() @@ -163,10 +164,12 @@ class BuildVolume(SceneNode): self._scene_objects = new_scene_objects self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered. - ## Updates the listeners that listen for changes in per-mesh stacks. - # - # \param node The node for which the decorators changed. def _updateNodeListeners(self, node: SceneNode): + """Updates the listeners that listen for changes in per-mesh stacks. + + :param node: The node for which the decorators changed. + """ + per_mesh_stack = node.callDecoration("getStack") if per_mesh_stack: per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged) @@ -187,10 +190,14 @@ class BuildVolume(SceneNode): if shape: self._shape = shape - ## Get the length of the 3D diagonal through the build volume. - # - # This gives a sense of the scale of the build volume in general. def getDiagonalSize(self) -> float: + """Get the length of the 3D diagonal through the build volume. + + This gives a sense of the scale of the build volume in general. + + :return: length of the 3D diagonal through the build volume + """ + return math.sqrt(self._width * self._width + self._height * self._height + self._depth * self._depth) def getDisallowedAreas(self) -> List[Polygon]: @@ -226,9 +233,9 @@ class BuildVolume(SceneNode): return True - ## For every sliceable node, update node._outside_buildarea - # def updateNodeBoundaryCheck(self): + """For every sliceable node, update node._outside_buildarea""" + if not self._global_container_stack: return @@ -295,8 +302,13 @@ class BuildVolume(SceneNode): for child_node in children: child_node.setOutsideBuildArea(group_node.isOutsideBuildArea()) - ## Update the outsideBuildArea of a single node, given bounds or current build volume def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None) -> None: + """Update the outsideBuildArea of a single node, given bounds or current build volume + + :param node: single node + :param bounds: bounds or current build volume + """ + if not isinstance(node, CuraSceneNode) or self._global_container_stack is None: return @@ -484,8 +496,9 @@ class BuildVolume(SceneNode): self._disallowed_area_size = max(size, self._disallowed_area_size) return mb.build() - ## Recalculates the build volume & disallowed areas. def rebuild(self) -> None: + """Recalculates the build volume & disallowed areas.""" + if not self._width or not self._height or not self._depth: return @@ -574,7 +587,7 @@ class BuildVolume(SceneNode): def _calculateExtraZClearance(self, extruders: List["ContainerStack"]) -> float: if not self._global_container_stack: return 0 - + extra_z = 0.0 for extruder in extruders: if extruder.getProperty("retraction_hop_enabled", "value"): @@ -586,8 +599,9 @@ class BuildVolume(SceneNode): def _onStackChanged(self): self._stack_change_timer.start() - ## Update the build volume visualization def _onStackChangeTimerFinished(self) -> None: + """Update the build volume visualization""" + if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) extruders = ExtruderManager.getInstance().getActiveExtruderStacks() @@ -712,15 +726,15 @@ class BuildVolume(SceneNode): self._depth = self._global_container_stack.getProperty("machine_depth", "value") self._shape = self._global_container_stack.getProperty("machine_shape", "value") - ## Calls _updateDisallowedAreas and makes sure the changes appear in the - # scene. - # - # This is required for a signal to trigger the update in one go. The - # ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``, - # since there may be other changes before it needs to be rebuilt, which - # would hit performance. - def _updateDisallowedAreasAndRebuild(self): + """Calls :py:meth:`cura.BuildVolume._updateDisallowedAreas` and makes sure the changes appear in the scene. + + This is required for a signal to trigger the update in one go. The + :py:meth:`cura.BuildVolume._updateDisallowedAreas` method itself shouldn't call + :py:meth:`cura.BuildVolume.rebuild`, since there may be other changes before it needs to be rebuilt, + which would hit performance. + """ + self._updateDisallowedAreas() self._updateRaftThickness() self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks()) @@ -782,15 +796,14 @@ class BuildVolume(SceneNode): for extruder_id in result_areas_no_brim: self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id]) - ## Computes the disallowed areas for objects that are printed with print - # features. - # - # This means that the brim, travel avoidance and such will be applied to - # these features. - # - # \return A dictionary with for each used extruder ID the disallowed areas - # where that extruder may not print. def _computeDisallowedAreasPrinted(self, used_extruders): + """Computes the disallowed areas for objects that are printed with print features. + + This means that the brim, travel avoidance and such will be applied to these features. + + :return: A dictionary with for each used extruder ID the disallowed areas where that extruder may not print. + """ + result = {} adhesion_extruder = None #type: ExtruderStack for extruder in used_extruders: @@ -828,18 +841,18 @@ class BuildVolume(SceneNode): return result - ## Computes the disallowed areas for the prime blobs. - # - # These are special because they are not subject to things like brim or - # travel avoidance. They do get a dilute with the border size though - # because they may not intersect with brims and such of other objects. - # - # \param border_size The size with which to offset the disallowed areas - # due to skirt, brim, travel avoid distance, etc. - # \param used_extruders The extruder stacks to generate disallowed areas - # for. - # \return A dictionary with for each used extruder ID the prime areas. def _computeDisallowedAreasPrimeBlob(self, border_size: float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]: + """Computes the disallowed areas for the prime blobs. + + These are special because they are not subject to things like brim or travel avoidance. They do get a dilute + with the border size though because they may not intersect with brims and such of other objects. + + :param border_size: The size with which to offset the disallowed areas due to skirt, brim, travel avoid distance + , etc. + :param used_extruders: The extruder stacks to generate disallowed areas for. + :return: A dictionary with for each used extruder ID the prime areas. + """ + result = {} # type: Dict[str, List[Polygon]] if not self._global_container_stack: return result @@ -867,19 +880,18 @@ class BuildVolume(SceneNode): return result - ## Computes the disallowed areas that are statically placed in the machine. - # - # It computes different disallowed areas depending on the offset of the - # extruder. The resulting dictionary will therefore have an entry for each - # extruder that is used. - # - # \param border_size The size with which to offset the disallowed areas - # due to skirt, brim, travel avoid distance, etc. - # \param used_extruders The extruder stacks to generate disallowed areas - # for. - # \return A dictionary with for each used extruder ID the disallowed areas - # where that extruder may not print. def _computeDisallowedAreasStatic(self, border_size:float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]: + """Computes the disallowed areas that are statically placed in the machine. + + It computes different disallowed areas depending on the offset of the extruder. The resulting dictionary will + therefore have an entry for each extruder that is used. + + :param border_size: The size with which to offset the disallowed areas due to skirt, brim, travel avoid distance + , etc. + :param used_extruders: The extruder stacks to generate disallowed areas for. + :return: A dictionary with for each used extruder ID the disallowed areas where that extruder may not print. + """ + # Convert disallowed areas to polygons and dilate them. machine_disallowed_polygons = [] if self._global_container_stack is None: @@ -1010,13 +1022,14 @@ class BuildVolume(SceneNode): return result - ## Private convenience function to get a setting from every extruder. - # - # For single extrusion machines, this gets the setting from the global - # stack. - # - # \return A sequence of setting values, one for each extruder. def _getSettingFromAllExtruders(self, setting_key: str) -> List[Any]: + """Private convenience function to get a setting from every extruder. + + For single extrusion machines, this gets the setting from the global stack. + + :return: A sequence of setting values, one for each extruder. + """ + all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value") all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type") for i, (setting_value, setting_type) in enumerate(zip(all_values, all_types)): @@ -1101,12 +1114,13 @@ class BuildVolume(SceneNode): return move_from_wall_radius - ## Calculate the disallowed radius around the edge. - # - # This disallowed radius is to allow for space around the models that is - # not part of the collision radius, such as bed adhesion (skirt/brim/raft) - # and travel avoid distance. def getEdgeDisallowedSize(self): + """Calculate the disallowed radius around the edge. + + This disallowed radius is to allow for space around the models that is not part of the collision radius, + such as bed adhesion (skirt/brim/raft) and travel avoid distance. + """ + if not self._global_container_stack or not self._global_container_stack.extruderList: return 0 diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index ab7a7ebdd1..4c0dd4855b 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -88,7 +88,7 @@ class CrashHandler: @staticmethod def pruneSensitiveData(obj: Any) -> Any: if isinstance(obj, str): - return obj.replace(home_dir, "") + return obj.replace("\\\\", "\\").replace(home_dir, "") if isinstance(obj, list): return [CrashHandler.pruneSensitiveData(item) for item in obj] if isinstance(obj, dict): @@ -150,8 +150,9 @@ class CrashHandler: self._sendCrashReport() os._exit(1) - ## Backup the current resource directories and create clean ones. def _backupAndStartClean(self): + """Backup the current resource directories and create clean ones.""" + Resources.factoryReset() self.early_crash_dialog.close() @@ -162,8 +163,9 @@ class CrashHandler: def _showDetailedReport(self): self.dialog.exec_() - ## Creates a modal dialog. def _createDialog(self): + """Creates a modal dialog.""" + self.dialog.setMinimumWidth(640) self.dialog.setMinimumHeight(640) self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report")) @@ -213,6 +215,16 @@ class CrashHandler: locale.getdefaultlocale()[0] self.data["locale_cura"] = self.cura_locale + try: + from cura.CuraApplication import CuraApplication + plugins = CuraApplication.getInstance().getPluginRegistry() + self.data["plugins"] = { + plugin_id: plugins.getMetaData(plugin_id)["plugin"]["version"] + for plugin_id in plugins.getInstalledPlugins() if not plugins.isBundledPlugin(plugin_id) + } + except: + self.data["plugins"] = {"[FAILED]": "0.0.0"} + crash_info = "" + catalog.i18nc("@label Cura version number", "Cura version") + ": " + str(self.cura_version) + "
" crash_info += "" + catalog.i18nc("@label", "Cura language") + ": " + str(self.cura_locale) + "
" crash_info += "" + catalog.i18nc("@label", "OS language") + ": " + str(self.data["locale_os"]) + "
" @@ -235,7 +247,9 @@ class CrashHandler: scope.set_tag("locale_os", self.data["locale_os"]) scope.set_tag("locale_cura", self.cura_locale) scope.set_tag("is_enterprise", ApplicationMetadata.IsEnterpriseVersion) - + + scope.set_context("plugins", self.data["plugins"]) + scope.set_user({"id": str(uuid.getnode())}) return group diff --git a/cura/CuraActions.py b/cura/CuraActions.py index 20c44c7916..4f3e842379 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -43,9 +43,10 @@ class CuraActions(QObject): event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues")], {}) cura.CuraApplication.CuraApplication.getInstance().functionEvent(event) - ## Reset camera position and direction to default @pyqtSlot() def homeCamera(self) -> None: + """Reset camera position and direction to default""" + scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene() camera = scene.getActiveCamera() if camera: @@ -54,9 +55,10 @@ class CuraActions(QObject): camera.setPerspective(True) camera.lookAt(Vector(0, 0, 0)) - ## Center all objects in the selection @pyqtSlot() def centerSelection(self) -> None: + """Center all objects in the selection""" + operation = GroupedOperation() for node in Selection.getAllSelectedObjects(): current_node = node @@ -73,18 +75,21 @@ class CuraActions(QObject): operation.addOperation(center_operation) operation.push() - ## Multiply all objects in the selection - # - # \param count The number of times to multiply the selection. @pyqtSlot(int) def multiplySelection(self, count: int) -> None: + """Multiply all objects in the selection + + :param count: The number of times to multiply the selection. + """ + min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8)) job.start() - ## Delete all selected objects. @pyqtSlot() def deleteSelection(self) -> None: + """Delete all selected objects.""" + if not cura.CuraApplication.CuraApplication.getInstance().getController().getToolsEnabled(): return @@ -106,11 +111,13 @@ class CuraActions(QObject): op.push() - ## Set the extruder that should be used to print the selection. - # - # \param extruder_id The ID of the extruder stack to use for the selected objects. @pyqtSlot(str) def setExtruderForSelection(self, extruder_id: str) -> None: + """Set the extruder that should be used to print the selection. + + :param extruder_id: The ID of the extruder stack to use for the selected objects. + """ + operation = GroupedOperation() nodes_to_change = [] diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 6dcd866c68..bae212917a 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -106,7 +106,7 @@ from cura.UI.RecommendedMode import RecommendedMode from cura.UI.TextManager import TextManager from cura.UI.WelcomePagesModel import WelcomePagesModel from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel -from cura.UltimakerCloud import UltimakerCloudAuthentication +from cura.UltimakerCloud import UltimakerCloudConstants from cura.Utils.NetworkingUtil import NetworkingUtil from . import BuildVolume from . import CameraAnimation @@ -207,6 +207,7 @@ class CuraApplication(QtApplication): self._first_start_machine_actions_model = None self._welcome_pages_model = WelcomePagesModel(self, parent = self) self._add_printer_pages_model = AddPrinterPagesModel(self, parent = self) + self._add_printer_pages_model_without_cancel = AddPrinterPagesModel(self, parent = self) self._whats_new_pages_model = WhatsNewPagesModel(self, parent = self) self._text_manager = TextManager(parent = self) @@ -255,15 +256,22 @@ class CuraApplication(QtApplication): @pyqtProperty(str, constant=True) def ultimakerCloudApiRootUrl(self) -> str: - return UltimakerCloudAuthentication.CuraCloudAPIRoot + return UltimakerCloudConstants.CuraCloudAPIRoot @pyqtProperty(str, constant = True) def ultimakerCloudAccountRootUrl(self) -> str: - return UltimakerCloudAuthentication.CuraCloudAccountAPIRoot + return UltimakerCloudConstants.CuraCloudAccountAPIRoot + + @pyqtProperty(str, constant=True) + def ultimakerDigitalFactoryUrl(self) -> str: + return UltimakerCloudConstants.CuraDigitalFactoryURL - # Adds command line options to the command line parser. This should be called after the application is created and - # before the pre-start. def addCommandLineOptions(self): + """Adds command line options to the command line parser. + + This should be called after the application is created and before the pre-start. + """ + super().addCommandLineOptions() self._cli_parser.add_argument("--help", "-h", action = "store_true", @@ -305,6 +313,9 @@ class CuraApplication(QtApplication): super().initialize() + self._preferences.addPreference("cura/single_instance", False) + self._use_single_instance = self._preferences.getValue("cura/single_instance") + self.__sendCommandToSingleInstance() self._initializeSettingDefinitions() self._initializeSettingFunctions() @@ -325,8 +336,9 @@ class CuraApplication(QtApplication): Logger.log("i", "Single instance commands were sent, exiting") sys.exit(0) - # Adds expected directory names and search paths for Resources. def __addExpectedResourceDirsAndSearchPaths(self): + """Adds expected directory names and search paths for Resources.""" + # this list of dir names will be used by UM to detect an old cura directory for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]: Resources.addExpectedDirNameInData(dir_name) @@ -368,9 +380,12 @@ class CuraApplication(QtApplication): SettingDefinition.addSettingType("[int]", None, str, None) - # Adds custom property types, settings types, and extra operators (functions) that need to be registered in - # SettingDefinition and SettingFunction. def _initializeSettingFunctions(self): + """Adds custom property types, settings types, and extra operators (functions). + + Whom need to be registered in SettingDefinition and SettingFunction. + """ + self._cura_formula_functions = CuraFormulaFunctions(self) SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder) @@ -380,8 +395,9 @@ class CuraApplication(QtApplication): SettingFunction.registerOperator("valueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndex) SettingFunction.registerOperator("extruderValueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndexInExtruder) - # Adds all resources and container related resources. def __addAllResourcesAndContainerResources(self) -> None: + """Adds all resources and container related resources.""" + Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality") Resources.addStorageType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes") Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants") @@ -406,8 +422,9 @@ class CuraApplication(QtApplication): Resources.addType(self.ResourceTypes.QmlFiles, "qml") Resources.addType(self.ResourceTypes.Firmware, "firmware") - # Adds all empty containers. def __addAllEmptyContainers(self) -> None: + """Adds all empty containers.""" + # Add empty variant, material and quality containers. # Since they are empty, they should never be serialized and instead just programmatically created. # We need them to simplify the switching between materials. @@ -432,9 +449,10 @@ class CuraApplication(QtApplication): self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_changes_container) self.empty_quality_changes_container = cura.Settings.cura_empty_instance_containers.empty_quality_changes_container - # Initializes the version upgrade manager with by providing the paths for each resource type and the latest - # versions. def __setLatestResouceVersionsForVersionUpgrade(self): + """Initializes the version upgrade manager with by providing the paths for each resource type and the latest + versions. """ + self._version_upgrade_manager.setCurrentVersions( { ("quality", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"), @@ -449,8 +467,9 @@ class CuraApplication(QtApplication): } ) - # Runs preparations that needs to be done before the starting process. def startSplashWindowPhase(self) -> None: + """Runs preparations that needs to be done before the starting process.""" + super().startSplashWindowPhase() if not self.getIsHeadLess(): @@ -509,7 +528,7 @@ class CuraApplication(QtApplication): # Set the setting version for Preferences preferences = self.getPreferences() preferences.addPreference("metadata/setting_version", 0) - preferences.setValue("metadata/setting_version", self.SettingVersion) #Don't make it equal to the default so that the setting version always gets written to the file. + preferences.setValue("metadata/setting_version", self.SettingVersion) # Don't make it equal to the default so that the setting version always gets written to the file. preferences.addPreference("cura/active_mode", "simple") @@ -613,12 +632,13 @@ class CuraApplication(QtApplication): def callConfirmExitDialogCallback(self, yes_or_no: bool) -> None: self._confirm_exit_dialog_callback(yes_or_no) - ## Signal to connect preferences action in QML showPreferencesWindow = pyqtSignal() + """Signal to connect preferences action in QML""" - ## Show the preferences window @pyqtSlot() def showPreferences(self) -> None: + """Show the preferences window""" + self.showPreferencesWindow.emit() # This is called by drag-and-dropping curapackage files. @@ -632,14 +652,13 @@ class CuraApplication(QtApplication): return self._global_container_stack @override(Application) - def setGlobalContainerStack(self, stack: "GlobalStack") -> None: + def setGlobalContainerStack(self, stack: Optional["GlobalStack"]) -> None: self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing Active Machine...")) super().setGlobalContainerStack(stack) - ## A reusable dialogbox - # showMessageBox = pyqtSignal(str,str, str, str, int, int, arguments = ["title", "text", "informativeText", "detailedText","buttons", "icon"]) + """A reusable dialogbox""" def messageBox(self, title, text, informativeText = "", @@ -717,9 +736,12 @@ class CuraApplication(QtApplication): def setDefaultPath(self, key, default_path): self.getPreferences().setValue("local_file/%s" % key, QUrl(default_path).toLocalFile()) - ## Handle loading of all plugin types (and the backend explicitly) - # \sa PluginRegistry def _loadPlugins(self) -> None: + """Handle loading of all plugin types (and the backend explicitly) + + :py:class:`Uranium.UM.PluginRegistry` + """ + self._plugin_registry.setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion) self._plugin_registry.addType("profile_reader", self._addProfileReader) @@ -734,7 +756,6 @@ class CuraApplication(QtApplication): if not hasattr(sys, "frozen"): self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins")) self._plugin_registry.loadPlugin("ConsoleLogger") - self._plugin_registry.loadPlugin("CuraEngineBackend") self._plugin_registry.loadPlugins() @@ -743,9 +764,12 @@ class CuraApplication(QtApplication): self._plugins_loaded = True - ## Set a short, user-friendly hint about current loading status. - # The way this message is displayed depends on application state def _setLoadingHint(self, hint: str): + """Set a short, user-friendly hint about current loading status. + + The way this message is displayed depends on application state + """ + if self.started: Logger.info(hint) else: @@ -792,6 +816,7 @@ class CuraApplication(QtApplication): self._output_device_manager.start() self._welcome_pages_model.initialize() self._add_printer_pages_model.initialize() + self._add_printer_pages_model_without_cancel.initialize(cancellable = False) self._whats_new_pages_model.initialize() # Detect in which mode to run and execute that mode @@ -829,13 +854,16 @@ class CuraApplication(QtApplication): self.callLater(self._openFile, file_name) initializationFinished = pyqtSignal() + showAddPrintersUncancellableDialog = pyqtSignal() # Used to show the add printers dialog with a greyed background - ## Run Cura without GUI elements and interaction (server mode). def runWithoutGUI(self): + """Run Cura without GUI elements and interaction (server mode).""" + self.closeSplash() - ## Run Cura with GUI (desktop mode). def runWithGUI(self): + """Run Cura with GUI (desktop mode).""" + self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Setting up scene...")) controller = self.getController() @@ -864,8 +892,9 @@ class CuraApplication(QtApplication): # Initialize camera tool camera_tool = controller.getTool("CameraTool") - camera_tool.setOrigin(Vector(0, 100, 0)) - camera_tool.setZoomRange(0.1, 2000) + if camera_tool: + camera_tool.setOrigin(Vector(0, 100, 0)) + camera_tool.setZoomRange(0.1, 2000) # Initialize camera animations self._camera_animation = CameraAnimation.CameraAnimation() @@ -916,6 +945,10 @@ class CuraApplication(QtApplication): def getAddPrinterPagesModel(self, *args) -> "AddPrinterPagesModel": return self._add_printer_pages_model + @pyqtSlot(result = QObject) + def getAddPrinterPagesModelWithoutCancel(self, *args) -> "AddPrinterPagesModel": + return self._add_printer_pages_model_without_cancel + @pyqtSlot(result = QObject) def getWhatsNewPagesModel(self, *args) -> "WhatsNewPagesModel": return self._whats_new_pages_model @@ -989,10 +1022,13 @@ class CuraApplication(QtApplication): self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager() return self._setting_inheritance_manager - ## Get the machine action manager - # 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. def getMachineActionManager(self, *args: Any) -> MachineActionManager.MachineActionManager: + """Get the machine action manager + + 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. + """ + return cast(MachineActionManager.MachineActionManager, self._machine_action_manager) @pyqtSlot(result = QObject) @@ -1012,8 +1048,9 @@ class CuraApplication(QtApplication): self._simple_mode_settings_manager = SimpleModeSettingsManager() return self._simple_mode_settings_manager - ## Handle Qt events def event(self, event): + """Handle Qt events""" + if event.type() == QEvent.FileOpen: if self._plugins_loaded: self._openFile(event.file()) @@ -1025,8 +1062,9 @@ class CuraApplication(QtApplication): def getAutoSave(self) -> Optional[AutoSave]: return self._auto_save - ## Get print information (duration / material used) def getPrintInformation(self): + """Get print information (duration / material used)""" + return self._print_information def getQualityProfilesDropDownMenuModel(self, *args, **kwargs): @@ -1042,10 +1080,12 @@ class CuraApplication(QtApplication): def getCuraAPI(self, *args, **kwargs) -> "CuraAPI": return self._cura_API - ## Registers objects for the QML engine to use. - # - # \param engine The QML engine. def registerObjects(self, engine): + """Registers objects for the QML engine to use. + + :param engine: The QML engine. + """ + super().registerObjects(engine) # global contexts @@ -1181,8 +1221,9 @@ class CuraApplication(QtApplication): if node is not None and (node.getMeshData() is not None or node.callDecoration("getLayerData")): self._update_platform_activity_timer.start() - ## Update scene bounding box for current build plate def updatePlatformActivity(self, node = None): + """Update scene bounding box for current build plate""" + count = 0 scene_bounding_box = None is_block_slicing_node = False @@ -1226,9 +1267,10 @@ class CuraApplication(QtApplication): self._platform_activity = True if count > 0 else False self.activityChanged.emit() - ## Select all nodes containing mesh data in the scene. @pyqtSlot() def selectAll(self): + """Select all nodes containing mesh data in the scene.""" + if not self.getController().getToolsEnabled(): return @@ -1247,9 +1289,10 @@ class CuraApplication(QtApplication): Selection.add(node) - ## Reset all translation on nodes with mesh data. @pyqtSlot() def resetAllTranslation(self): + """Reset all translation on nodes with mesh data.""" + Logger.log("i", "Resetting all scene translations") nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): @@ -1275,9 +1318,10 @@ class CuraApplication(QtApplication): op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0))) op.push() - ## Reset all transformations on nodes with mesh data. @pyqtSlot() def resetAll(self): + """Reset all transformations on nodes with mesh data.""" + Logger.log("i", "Resetting all scene transformations") nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): @@ -1303,9 +1347,10 @@ class CuraApplication(QtApplication): op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1))) op.push() - ## Arrange all objects. @pyqtSlot() def arrangeObjectsToAllBuildPlates(self) -> None: + """Arrange all objects.""" + nodes_to_arrange = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if not isinstance(node, SceneNode): @@ -1358,17 +1403,21 @@ class CuraApplication(QtApplication): nodes_to_arrange.append(node) self.arrange(nodes_to_arrange, fixed_nodes = []) - ## Arrange a set of nodes given a set of fixed nodes - # \param nodes nodes that we have to place - # \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None: + """Arrange a set of nodes given a set of fixed nodes + + :param nodes: nodes that we have to place + :param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes + """ + min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8)) job.start() - ## Reload all mesh data on the screen from file. @pyqtSlot() def reloadAll(self) -> None: + """Reload all mesh data on the screen from file.""" + Logger.log("i", "Reloading all loaded mesh data.") nodes = [] has_merged_nodes = False @@ -1478,8 +1527,9 @@ class CuraApplication(QtApplication): group_node.setName("MergedMesh") # add a specific name to distinguish this node - ## Updates origin position of all merged meshes def updateOriginOfMergedMeshes(self, _): + """Updates origin position of all merged meshes""" + group_nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh": @@ -1597,9 +1647,10 @@ class CuraApplication(QtApplication): scene from its source file. The function gets all the nodes that exist in the file through the job result, and then finds the scene node that it wants to refresh by its object id. Each job refreshes only one node. - :param job: The ReadMeshJob running in the background that reads all the meshes in a file - :return: None + :param job: The :py:class:`Uranium.UM.ReadMeshJob.ReadMeshJob` running in the background that reads all the + meshes in a file """ + job_result = job.getResult() # nodes that exist inside the file read by this job if len(job_result) == 0: Logger.log("e", "Reloading the mesh failed.") @@ -1645,12 +1696,15 @@ class CuraApplication(QtApplication): def additionalComponents(self): return self._additional_components - ## Add a component to a list of components to be reparented to another area in the GUI. - # The actual reparenting is done by the area itself. - # \param area_id \type{str} Identifying name of the area to which the component should be reparented - # \param component \type{QQuickComponent} The component that should be reparented @pyqtSlot(str, "QVariant") - def addAdditionalComponent(self, area_id, component): + def addAdditionalComponent(self, area_id: str, component): + """Add a component to a list of components to be reparented to another area in the GUI. + + The actual reparenting is done by the area itself. + :param area_id: dentifying name of the area to which the component should be reparented + :param (QQuickComponent) component: The component that should be reparented + """ + if area_id not in self._additional_components: self._additional_components[area_id] = [] self._additional_components[area_id].append(component) @@ -1665,10 +1719,13 @@ class CuraApplication(QtApplication): @pyqtSlot(QUrl, str) @pyqtSlot(QUrl) - ## Open a local file - # \param project_mode How to handle project files. Either None(default): Follow user preference, "open_as_model" or - # "open_as_project". This parameter is only considered if the file is a project file. def readLocalFile(self, file: QUrl, project_mode: Optional[str] = None): + """Open a local file + + :param project_mode: How to handle project files. Either None(default): Follow user preference, "open_as_model" + or "open_as_project". This parameter is only considered if the file is a project file. + """ + if not file.isValid(): return @@ -1844,9 +1901,8 @@ class CuraApplication(QtApplication): @pyqtSlot(str, result=bool) def checkIsValidProjectFile(self, file_url): - """ - Checks if the given file URL is a valid project file. - """ + """Checks if the given file URL is a valid project file. """ + file_path = QUrl(file_url).toLocalFile() workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path) if workspace_reader is None: @@ -1864,7 +1920,6 @@ class CuraApplication(QtApplication): return selection_pass = cast(SelectionPass, self.getRenderer().getRenderPass("selection")) if not selection_pass: # If you right-click before the rendering has been initialised there might not be a selection pass yet. - print("--------------ding! Got the crash.") return node = self.getController().getScene().findObject(selection_pass.getIdAtPosition(x, y)) if not node: diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index a0d3a8d44a..26d6591099 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -24,11 +24,15 @@ class CuraPackageManager(PackageManager): super().initialize() - ## Returns a list of where the package is used - # empty if it is never used. - # It loops through all the package contents and see if some of the ids are used. - # The list consists of 3-tuples: (global_stack, extruder_nr, container_id) def getMachinesUsingPackage(self, package_id: str) -> Tuple[List[Tuple[GlobalStack, str, str]], List[Tuple[GlobalStack, str, str]]]: + """Returns a list of where the package is used + + It loops through all the package contents and see if some of the ids are used. + + :param package_id: package id to search for + :return: empty if it is never used, otherwise a list consisting of 3-tuples + """ + ids = self.getPackageContainerIds(package_id) container_stacks = self._application.getContainerRegistry().findContainerStacks() global_stacks = [container_stack for container_stack in container_stacks if isinstance(container_stack, GlobalStack)] @@ -36,10 +40,10 @@ class CuraPackageManager(PackageManager): machine_with_qualities = [] for container_id in ids: for global_stack in global_stacks: - for extruder_nr, extruder_stack in global_stack.extruders.items(): + for extruder_nr, extruder_stack in enumerate(global_stack.extruderList): if container_id in (extruder_stack.material.getId(), extruder_stack.material.getMetaData().get("base_file")): - machine_with_materials.append((global_stack, extruder_nr, container_id)) + machine_with_materials.append((global_stack, str(extruder_nr), container_id)) if container_id == extruder_stack.quality.getId(): - machine_with_qualities.append((global_stack, extruder_nr, container_id)) + machine_with_qualities.append((global_stack, str(extruder_nr), container_id)) return machine_with_materials, machine_with_qualities diff --git a/cura/Layer.py b/cura/Layer.py index 933d4436c9..af42488e2a 100644 --- a/cura/Layer.py +++ b/cura/Layer.py @@ -76,7 +76,7 @@ class Layer: def createMeshOrJumps(self, make_mesh: bool) -> MeshData: builder = MeshBuilder() - + line_count = 0 if make_mesh: for polygon in self._polygons: @@ -87,7 +87,7 @@ class Layer: # Reserve the necessary space for the data upfront builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count) - + for polygon in self._polygons: # Filter out the types of lines we are not interested in depending on whether we are drawing the mesh or the jumps. index_mask = numpy.logical_not(polygon.jumpMask) if make_mesh else polygon.jumpMask @@ -96,7 +96,7 @@ class Layer: points = numpy.concatenate((polygon.data[:-1], polygon.data[1:]), 1)[index_mask.ravel()] # Line types of the points we want to draw line_types = polygon.types[index_mask] - + # Shift the z-axis according to previous implementation. if make_mesh: points[polygon.isInfillOrSkinType(line_types), 1::3] -= 0.01 @@ -118,5 +118,5 @@ class Layer: f_colors = numpy.repeat(polygon.mapLineTypeToColor(line_types), 4, 0) builder.addFacesWithColor(f_points, f_indices, f_colors) - + return builder.build() \ No newline at end of file diff --git a/cura/LayerData.py b/cura/LayerData.py index 72824591ab..e58fda597a 100644 --- a/cura/LayerData.py +++ b/cura/LayerData.py @@ -3,9 +3,12 @@ from UM.Mesh.MeshData import MeshData -## Class to holds the layer mesh and information about the layers. -# Immutable, use LayerDataBuilder to create one of these. class LayerData(MeshData): + """Class to holds the layer mesh and information about the layers. + + Immutable, use :py:class:`cura.LayerDataBuilder.LayerDataBuilder` to create one of these. + """ + def __init__(self, vertices = None, normals = None, indices = None, colors = None, uvs = None, file_name = None, center_position = None, layers=None, element_counts=None, attributes=None): super().__init__(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs, diff --git a/cura/LayerDataBuilder.py b/cura/LayerDataBuilder.py index e8d1b8c59f..d8801c9e7b 100755 --- a/cura/LayerDataBuilder.py +++ b/cura/LayerDataBuilder.py @@ -10,8 +10,9 @@ import numpy from typing import Dict, Optional -## Builder class for constructing a LayerData object class LayerDataBuilder(MeshBuilder): + """Builder class for constructing a :py:class:`cura.LayerData.LayerData` object""" + def __init__(self) -> None: super().__init__() self._layers = {} # type: Dict[int, Layer] @@ -42,11 +43,13 @@ class LayerDataBuilder(MeshBuilder): self._layers[layer].setThickness(thickness) - ## Return the layer data as LayerData. - # - # \param material_color_map: [r, g, b, a] for each extruder row. - # \param line_type_brightness: compatibility layer view uses line type brightness of 0.5 def build(self, material_color_map, line_type_brightness = 1.0): + """Return the layer data as :py:class:`cura.LayerData.LayerData`. + + :param material_color_map: [r, g, b, a] for each extruder row. + :param line_type_brightness: compatibility layer view uses line type brightness of 0.5 + """ + vertex_count = 0 index_count = 0 for layer, data in self._layers.items(): diff --git a/cura/LayerDataDecorator.py b/cura/LayerDataDecorator.py index 36466cac72..66924e8d2c 100644 --- a/cura/LayerDataDecorator.py +++ b/cura/LayerDataDecorator.py @@ -7,8 +7,9 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from cura.LayerData import LayerData -## Simple decorator to indicate a scene node holds layer data. class LayerDataDecorator(SceneNodeDecorator): + """Simple decorator to indicate a scene node holds layer data.""" + def __init__(self) -> None: super().__init__() self._layer_data = None # type: Optional[LayerData] diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py index 70d818f1ca..6e518e984a 100644 --- a/cura/LayerPolygon.py +++ b/cura/LayerPolygon.py @@ -26,14 +26,17 @@ class LayerPolygon: __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType) - ## LayerPolygon, used in ProcessSlicedLayersJob - # \param extruder The position of the extruder - # \param line_types array with line_types - # \param data new_points - # \param line_widths array with line widths - # \param line_thicknesses: array with type as index and thickness as value - # \param line_feedrates array with line feedrates def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray, line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None: + """LayerPolygon, used in ProcessSlicedLayersJob + + :param extruder: The position of the extruder + :param line_types: array with line_types + :param data: new_points + :param line_widths: array with line widths + :param line_thicknesses: array with type as index and thickness as value + :param line_feedrates: array with line feedrates + """ + self._extruder = extruder self._types = line_types for i in range(len(self._types)): @@ -59,21 +62,21 @@ class LayerPolygon: # re-used and can save alot of memory usage. self._color_map = LayerPolygon.getColorMap() self._colors = self._color_map[self._types] # type: numpy.ndarray - + # 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. 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_needed_points = None # type: Optional[numpy.ndarray] - + def buildCache(self) -> None: # For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out. self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype = bool) mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask) self._index_begin = 0 self._index_end = mesh_line_count - + self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype = numpy.bool) # Only if the type of line segment changes do we need to add an extra vertex to change colors self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1] @@ -83,19 +86,22 @@ class LayerPolygon: self._vertex_begin = 0 self._vertex_end = numpy.sum( self._build_cache_needed_points ) - ## Set all the arrays provided by the function caller, representing the LayerPolygon - # The arrays are either by vertex or by indices. - # - # \param vertex_offset : determines where to start and end filling the arrays - # \param index_offset : determines where to start and end filling the arrays - # \param vertices : vertex numpy array to be filled - # \param colors : vertex numpy array to be filled - # \param line_dimensions : vertex numpy array to be filled - # \param feedrates : vertex numpy array to be filled - # \param extruders : vertex numpy array to be filled - # \param line_types : vertex numpy array to be filled - # \param indices : index numpy array to be filled def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray, colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray, extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None: + """Set all the arrays provided by the function caller, representing the LayerPolygon + + The arrays are either by vertex or by indices. + + :param vertex_offset: determines where to start and end filling the arrays + :param index_offset: determines where to start and end filling the arrays + :param vertices: vertex numpy array to be filled + :param colors: vertex numpy array to be filled + :param line_dimensions: vertex numpy array to be filled + :param feedrates: vertex numpy array to be filled + :param extruders: vertex numpy array to be filled + :param line_types: vertex numpy array to be filled + :param indices: index numpy array to be filled + """ + if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None: self.buildCache() @@ -105,16 +111,16 @@ class LayerPolygon: line_mesh_mask = self._build_cache_line_mesh_mask needed_points_list = self._build_cache_needed_points - + # Index to the points we need to represent the line mesh. This is constructed by generating simple # start and end points for each line. For line segment n these are points n and n+1. Row n reads [n n+1] # Then then the indices for the points we don't need are thrown away based on the pre-calculated list. index_list = ( numpy.arange(len(self._types)).reshape((-1, 1)) + numpy.array([[0, 1]]) ).reshape((-1, 1))[needed_points_list.reshape((-1, 1))] - + # The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset. self._vertex_begin += vertex_offset self._vertex_end += vertex_offset - + # Points are picked based on the index list to get the vertices needed. vertices[self._vertex_begin:self._vertex_end, :] = self._data[index_list, :] @@ -136,14 +142,14 @@ class LayerPolygon: # The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset. self._index_begin += index_offset self._index_end += index_offset - + indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype = numpy.int32).reshape((-1, 1)) # When the line type changes the index needs to be increased by 2. indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1)) # Each line segment goes from it's starting point p to p+1, offset by the vertex index. # The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above. indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin]) - + self._build_cache_line_mesh_mask = None self._build_cache_needed_points = None @@ -189,7 +195,7 @@ class LayerPolygon: @property def lineFeedrates(self): return self._line_feedrates - + @property def jumpMask(self): return self._jump_mask @@ -202,8 +208,12 @@ class LayerPolygon: def jumpCount(self): return self._jump_count - # Calculate normals for the entire polygon using numpy. def getNormals(self) -> numpy.ndarray: + """Calculate normals for the entire polygon using numpy. + + :return: normals for the entire polygon + """ + normals = numpy.copy(self._data) normals[:, 1] = 0.0 # We are only interested in 2D normals @@ -229,9 +239,10 @@ class LayerPolygon: __color_map = None # type: numpy.ndarray - ## Gets the instance of the VersionUpgradeManager, or creates one. @classmethod def getColorMap(cls) -> numpy.ndarray: + """Gets the instance of the VersionUpgradeManager, or creates one.""" + if cls.__color_map is None: theme = cast(Theme, QtApplication.getInstance().getTheme()) cls.__color_map = numpy.array([ diff --git a/cura/MachineAction.py b/cura/MachineAction.py index 0f05401c89..74b742ef4d 100644 --- a/cura/MachineAction.py +++ b/cura/MachineAction.py @@ -11,16 +11,22 @@ from UM.PluginObject import PluginObject from UM.PluginRegistry import PluginRegistry -## Machine actions are actions that are added to a specific machine type. Examples of such actions are -# updating the firmware, connecting with remote devices or doing bed leveling. A machine action can also have a -# qml, which should contain a "Cura.MachineAction" item. When activated, the item will be displayed in a dialog -# and this object will be added as "manager" (so all pyqtSlot() functions can be called by calling manager.func()) class MachineAction(QObject, PluginObject): + """Machine actions are actions that are added to a specific machine type. + + Examples of such actions are updating the firmware, connecting with remote devices or doing bed leveling. A + machine action can also have a qml, which should contain a :py:class:`cura.MachineAction.MachineAction` item. + When activated, the item will be displayed in a dialog and this object will be added as "manager" (so all + pyqtSlot() functions can be called by calling manager.func()) + """ - ## Create a new Machine action. - # \param key unique key of the machine action - # \param label Human readable label used to identify the machine action. def __init__(self, key: str, label: str = "") -> None: + """Create a new Machine action. + + :param key: unique key of the machine action + :param label: Human readable label used to identify the machine action. + """ + super().__init__() self._key = key self._label = label @@ -34,10 +40,14 @@ class MachineAction(QObject, PluginObject): def getKey(self) -> str: return self._key - ## Whether this action needs to ask the user anything. - # If not, we shouldn't present the user with certain screens which otherwise show up. - # Defaults to true to be in line with the old behaviour. def needsUserInteraction(self) -> bool: + """Whether this action needs to ask the user anything. + + If not, we shouldn't present the user with certain screens which otherwise show up. + + :return: Defaults to true to be in line with the old behaviour. + """ + return True @pyqtProperty(str, notify = labelChanged) @@ -49,17 +59,24 @@ class MachineAction(QObject, PluginObject): self._label = label self.labelChanged.emit() - ## Reset the action to it's default state. - # This should not be re-implemented by child classes, instead re-implement _reset. - # /sa _reset @pyqtSlot() def reset(self) -> None: + """Reset the action to it's default state. + + This should not be re-implemented by child classes, instead re-implement _reset. + + :py:meth:`cura.MachineAction.MachineAction._reset` + """ + self._finished = False self._reset() - ## Protected implementation of reset. - # /sa reset() def _reset(self) -> None: + """Protected implementation of reset. + + See also :py:meth:`cura.MachineAction.MachineAction.reset` + """ + pass @pyqtSlot() @@ -72,8 +89,9 @@ class MachineAction(QObject, PluginObject): def finished(self) -> bool: return self._finished - ## Protected helper to create a view object based on provided QML. def _createViewFromQML(self) -> Optional["QObject"]: + """Protected helper to create a view object based on provided QML.""" + plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) if plugin_path is None: Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId()) diff --git a/cura/Machines/ContainerNode.py b/cura/Machines/ContainerNode.py index 8a9ddcc39b..5341edd0c6 100644 --- a/cura/Machines/ContainerNode.py +++ b/cura/Machines/ContainerNode.py @@ -9,47 +9,59 @@ from UM.Logger import Logger from UM.Settings.InstanceContainer import InstanceContainer -## A node in the container tree. It represents one container. -# -# The container it represents is referenced by its container_id. During normal -# use of the tree, this container is not constructed. Only when parts of the -# tree need to get loaded in the container stack should it get constructed. class ContainerNode: - ## Creates a new node for the container tree. - # \param container_id The ID of the container that this node should - # represent. + """A node in the container tree. It represents one container. + + The container it represents is referenced by its container_id. During normal use of the tree, this container is + not constructed. Only when parts of the tree need to get loaded in the container stack should it get constructed. + """ + def __init__(self, container_id: str) -> None: + """Creates a new node for the container tree. + + :param container_id: The ID of the container that this node should represent. + """ + self.container_id = container_id self._container = None # type: Optional[InstanceContainer] self.children_map = {} # type: Dict[str, ContainerNode] # Mapping from container ID to container node. - ## Gets the metadata of the container that this node represents. - # Getting the metadata from the container directly is about 10x as fast. - # \return The metadata of the container in this node. def getMetadata(self) -> Dict[str, Any]: + """Gets the metadata of the container that this node represents. + + Getting the metadata from the container directly is about 10x as fast. + + :return: The metadata of the container in this node. + """ + return ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)[0] - ## Get an entry from the metadata of the container that this node contains. - # - # This is just a convenience function. - # \param entry The metadata entry key to return. - # \param default If the metadata is not present or the container is not - # found, the value of this default is returned. - # \return The value of the metadata entry, or the default if it was not - # present. def getMetaDataEntry(self, entry: str, default: Any = None) -> Any: + """Get an entry from the metadata of the container that this node contains. + + This is just a convenience function. + + :param entry: The metadata entry key to return. + :param default: If the metadata is not present or the container is not found, the value of this default is + returned. + + :return: The value of the metadata entry, or the default if it was not present. + """ + container_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id) if len(container_metadata) == 0: return default return container_metadata[0].get(entry, default) - ## The container that this node's container ID refers to. - # - # This can be used to finally instantiate the container in order to put it - # in the container stack. - # \return A container. @property def container(self) -> Optional[InstanceContainer]: + """The container that this node's container ID refers to. + + This can be used to finally instantiate the container in order to put it in the container stack. + + :return: A container. + """ + if not self._container: container_list = ContainerRegistry.getInstance().findInstanceContainers(id = self.container_id) if len(container_list) == 0: diff --git a/cura/Machines/ContainerTree.py b/cura/Machines/ContainerTree.py index a7bb0610bd..7f1e7900d0 100644 --- a/cura/Machines/ContainerTree.py +++ b/cura/Machines/ContainerTree.py @@ -19,17 +19,16 @@ if TYPE_CHECKING: from UM.Settings.ContainerStack import ContainerStack -## This class contains a look-up tree for which containers are available at -# which stages of configuration. -# -# The tree starts at the machine definitions. For every distinct definition -# there will be one machine node here. -# -# All of the fallbacks for material choices, quality choices, etc. should be -# encoded in this tree. There must always be at least one child node (for -# nodes that have children) but that child node may be a node representing the -# empty instance container. class ContainerTree: + """This class contains a look-up tree for which containers are available at which stages of configuration. + + The tree starts at the machine definitions. For every distinct definition there will be one machine node here. + + All of the fallbacks for material choices, quality choices, etc. should be encoded in this tree. There must + always be at least one child node (for nodes that have children) but that child node may be a node representing + the empty instance container. + """ + __instance = None # type: Optional["ContainerTree"] @classmethod @@ -43,13 +42,15 @@ class ContainerTree: self.materialsChanged = Signal() # Emitted when any of the material nodes in the tree got changed. cura.CuraApplication.CuraApplication.getInstance().initializationFinished.connect(self._onStartupFinished) # Start the background task to load more machine nodes after start-up is completed. - ## Get the quality groups available for the currently activated printer. - # - # This contains all quality groups, enabled or disabled. To check whether - # the quality group can be activated, test for the - # ``QualityGroup.is_available`` property. - # \return For every quality type, one quality group. def getCurrentQualityGroups(self) -> Dict[str, "QualityGroup"]: + """Get the quality groups available for the currently activated printer. + + This contains all quality groups, enabled or disabled. To check whether the quality group can be activated, + test for the ``QualityGroup.is_available`` property. + + :return: For every quality type, one quality group. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return {} @@ -58,14 +59,15 @@ class ContainerTree: extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList] return self.machines[global_stack.definition.getId()].getQualityGroups(variant_names, material_bases, extruder_enabled) - ## Get the quality changes groups available for the currently activated - # printer. - # - # This contains all quality changes groups, enabled or disabled. To check - # whether the quality changes group can be activated, test for the - # ``QualityChangesGroup.is_available`` property. - # \return A list of all quality changes groups. def getCurrentQualityChangesGroups(self) -> List["QualityChangesGroup"]: + """Get the quality changes groups available for the currently activated printer. + + This contains all quality changes groups, enabled or disabled. To check whether the quality changes group can + be activated, test for the ``QualityChangesGroup.is_available`` property. + + :return: A list of all quality changes groups. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return [] @@ -74,31 +76,43 @@ class ContainerTree: extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList] return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled) - ## Ran after completely starting up the application. def _onStartupFinished(self) -> None: + """Ran after completely starting up the application.""" + currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks. JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added)) - ## Dictionary-like object that contains the machines. - # - # This handles the lazy loading of MachineNodes. class _MachineNodeMap: + """Dictionary-like object that contains the machines. + + This handles the lazy loading of MachineNodes. + """ + def __init__(self) -> None: self._machines = {} # type: Dict[str, MachineNode] - ## Returns whether a printer with a certain definition ID exists. This - # is regardless of whether or not the printer is loaded yet. - # \param definition_id The definition to look for. - # \return Whether or not a printer definition exists with that name. def __contains__(self, definition_id: str) -> bool: + """Returns whether a printer with a certain definition ID exists. + + This is regardless of whether or not the printer is loaded yet. + + :param definition_id: The definition to look for. + + :return: Whether or not a printer definition exists with that name. + """ + return len(ContainerRegistry.getInstance().findContainersMetadata(id = definition_id)) > 0 - ## Returns a machine node for the specified definition ID. - # - # If the machine node wasn't loaded yet, this will load it lazily. - # \param definition_id The definition to look for. - # \return A machine node for that definition. def __getitem__(self, definition_id: str) -> MachineNode: + """Returns a machine node for the specified definition ID. + + If the machine node wasn't loaded yet, this will load it lazily. + + :param definition_id: The definition to look for. + + :return: A machine node for that definition. + """ + if definition_id not in self._machines: start_time = time.time() self._machines[definition_id] = MachineNode(definition_id) @@ -106,46 +120,58 @@ class ContainerTree: Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - start_time)) return self._machines[definition_id] - ## Gets a machine node for the specified definition ID, with default. - # - # The default is returned if there is no definition with the specified - # ID. If the machine node wasn't loaded yet, this will load it lazily. - # \param definition_id The definition to look for. - # \param default The machine node to return if there is no machine - # with that definition (can be ``None`` optionally or if not - # provided). - # \return A machine node for that definition, or the default if there - # is no definition with the provided definition_id. def get(self, definition_id: str, default: Optional[MachineNode] = None) -> Optional[MachineNode]: + """Gets a machine node for the specified definition ID, with default. + + The default is returned if there is no definition with the specified ID. If the machine node wasn't + loaded yet, this will load it lazily. + + :param definition_id: The definition to look for. + :param default: The machine node to return if there is no machine with that definition (can be ``None`` + optionally or if not provided). + + :return: A machine node for that definition, or the default if there is no definition with the provided + definition_id. + """ + if definition_id not in self: return default return self[definition_id] - ## Returns whether we've already cached this definition's node. - # \param definition_id The definition that we may have cached. - # \return ``True`` if it's cached. def is_loaded(self, definition_id: str) -> bool: + """Returns whether we've already cached this definition's node. + + :param definition_id: The definition that we may have cached. + + :return: ``True`` if it's cached. + """ + return definition_id in self._machines - ## Pre-loads all currently added printers as a background task so that - # switching printers in the interface is faster. class _MachineNodeLoadJob(Job): - ## Creates a new background task. - # \param tree_root The container tree instance. This cannot be - # obtained through the singleton static function since the instance - # may not yet be constructed completely. - # \param container_stacks All of the stacks to pre-load the container - # trees for. This needs to be provided from here because the stacks - # need to be constructed on the main thread because they are QObject. + """Pre-loads all currently added printers as a background task so that switching printers in the interface is + faster. + """ + def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]) -> None: + """Creates a new background task. + + :param tree_root: The container tree instance. This cannot be obtained through the singleton static + function since the instance may not yet be constructed completely. + :param container_stacks: All of the stacks to pre-load the container trees for. This needs to be provided + from here because the stacks need to be constructed on the main thread because they are QObject. + """ + self.tree_root = tree_root self.container_stacks = container_stacks super().__init__() - ## Starts the background task. - # - # The ``JobQueue`` will schedule this on a different thread. def run(self) -> None: + """Starts the background task. + + The ``JobQueue`` will schedule this on a different thread. + """ + for stack in self.container_stacks: # Load all currently-added containers. if not isinstance(stack, GlobalStack): continue diff --git a/cura/Machines/IntentNode.py b/cura/Machines/IntentNode.py index 2b3a596f81..949a5d3a2b 100644 --- a/cura/Machines/IntentNode.py +++ b/cura/Machines/IntentNode.py @@ -11,10 +11,12 @@ if TYPE_CHECKING: from cura.Machines.QualityNode import QualityNode -## This class represents an intent profile in the container tree. -# -# This class has no more subnodes. class IntentNode(ContainerNode): + """This class represents an intent profile in the container tree. + + This class has no more subnodes. + """ + def __init__(self, container_id: str, quality: "QualityNode") -> None: super().__init__(container_id) self.quality = quality diff --git a/cura/Machines/MachineErrorChecker.py b/cura/Machines/MachineErrorChecker.py index 7a5291dac5..bc9ef723d4 100644 --- a/cura/Machines/MachineErrorChecker.py +++ b/cura/Machines/MachineErrorChecker.py @@ -13,16 +13,16 @@ from UM.Settings.SettingDefinition import SettingDefinition from UM.Settings.Validator import ValidatorState import cura.CuraApplication -# -# This class performs setting error checks for the currently active machine. -# -# The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag. -# The idea here is to split the whole error check into small tasks, each of which only checks a single setting key -# in a stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should -# be good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait -# for it to finish the complete work. -# + class MachineErrorChecker(QObject): + """This class performs setting error checks for the currently active machine. + + The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag. The idea + here is to split the whole error check into small tasks, each of which only checks a single setting key in a + stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should be + good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait + for it to finish the complete work. + """ def __init__(self, parent: Optional[QObject] = None) -> None: super().__init__(parent) @@ -92,24 +92,37 @@ class MachineErrorChecker(QObject): def needToWaitForResult(self) -> bool: return self._need_to_check or self._check_in_progress - # Start the error check for property changed - # this is seperate from the startErrorCheck because it ignores a number property types def startErrorCheckPropertyChanged(self, key: str, property_name: str) -> None: + """Start the error check for property changed + + this is seperate from the startErrorCheck because it ignores a number property types + + :param key: + :param property_name: + """ + if property_name != "value": return self.startErrorCheck() - # Starts the error check timer to schedule a new error check. def startErrorCheck(self, *args: Any) -> None: + """Starts the error check timer to schedule a new error check. + + :param args: + """ + if not self._check_in_progress: self._need_to_check = True self.needToWaitForResultChanged.emit() self._error_check_timer.start() - # This function is called by the timer to reschedule a new error check. - # If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag - # to notify the current check to stop and start a new one. def _rescheduleCheck(self) -> None: + """This function is called by the timer to reschedule a new error check. + + If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag + to notify the current check to stop and start a new one. + """ + if self._check_in_progress and not self._need_to_check: self._need_to_check = True self.needToWaitForResultChanged.emit() diff --git a/cura/Machines/MachineNode.py b/cura/Machines/MachineNode.py index 6a415b01c4..d487ea57f2 100644 --- a/cura/Machines/MachineNode.py +++ b/cura/Machines/MachineNode.py @@ -17,10 +17,12 @@ from cura.Machines.VariantNode import VariantNode import UM.FlameProfiler -## This class represents a machine in the container tree. -# -# The subnodes of these nodes are variants. class MachineNode(ContainerNode): + """This class represents a machine in the container tree. + + The subnodes of these nodes are variants. + """ + def __init__(self, container_id: str) -> None: super().__init__(container_id) self.variants = {} # type: Dict[str, VariantNode] # Mapping variant names to their nodes. @@ -47,20 +49,21 @@ class MachineNode(ContainerNode): self._loadAll() - ## Get the available quality groups for this machine. - # - # This returns all quality groups, regardless of whether they are - # available to the combination of extruders or not. On the resulting - # quality groups, the is_available property is set to indicate whether the - # quality group can be selected according to the combination of extruders - # in the parameters. - # \param variant_names The names of the variants loaded in each extruder. - # \param material_bases The base file names of the materials loaded in - # each extruder. - # \param extruder_enabled Whether or not the extruders are enabled. This - # allows the function to set the is_available properly. - # \return For each available quality type, a QualityGroup instance. def getQualityGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> Dict[str, QualityGroup]: + """Get the available quality groups for this machine. + + This returns all quality groups, regardless of whether they are available to the combination of extruders or + not. On the resulting quality groups, the is_available property is set to indicate whether the quality group + can be selected according to the combination of extruders in the parameters. + + :param variant_names: The names of the variants loaded in each extruder. + :param material_bases: The base file names of the materials loaded in each extruder. + :param extruder_enabled: Whether or not the extruders are enabled. This allows the function to set the + is_available properly. + + :return: For each available quality type, a QualityGroup instance. + """ + if len(variant_names) != len(material_bases) or len(variant_names) != len(extruder_enabled): Logger.log("e", "The number of extruders in the list of variants (" + str(len(variant_names)) + ") is not equal to the number of extruders in the list of materials (" + str(len(material_bases)) + ") or the list of enabled extruders (" + str(len(extruder_enabled)) + ").") return {} @@ -98,28 +101,26 @@ class MachineNode(ContainerNode): quality_groups[quality_type].is_available = True return quality_groups - ## Returns all of the quality changes groups available to this printer. - # - # The quality changes groups store which quality type and intent category - # they were made for, but not which material and nozzle. Instead for the - # quality type and intent category, the quality changes will always be - # available but change the quality type and intent category when - # activated. - # - # The quality changes group does depend on the printer: Which quality - # definition is used. - # - # The quality changes groups that are available do depend on the quality - # types that are available, so it must still be known which extruders are - # enabled and which materials and variants are loaded in them. This allows - # setting the correct is_available flag. - # \param variant_names The names of the variants loaded in each extruder. - # \param material_bases The base file names of the materials loaded in - # each extruder. - # \param extruder_enabled For each extruder whether or not they are - # enabled. - # \return List of all quality changes groups for the printer. def getQualityChangesGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> List[QualityChangesGroup]: + """Returns all of the quality changes groups available to this printer. + + The quality changes groups store which quality type and intent category they were made for, but not which + material and nozzle. Instead for the quality type and intent category, the quality changes will always be + available but change the quality type and intent category when activated. + + The quality changes group does depend on the printer: Which quality definition is used. + + The quality changes groups that are available do depend on the quality types that are available, so it must + still be known which extruders are enabled and which materials and variants are loaded in them. This allows + setting the correct is_available flag. + + :param variant_names: The names of the variants loaded in each extruder. + :param material_bases: The base file names of the materials loaded in each extruder. + :param extruder_enabled: For each extruder whether or not they are enabled. + + :return: List of all quality changes groups for the printer. + """ + machine_quality_changes = ContainerRegistry.getInstance().findContainersMetadata(type = "quality_changes", definition = self.quality_definition) # All quality changes for each extruder. groups_by_name = {} #type: Dict[str, QualityChangesGroup] # Group quality changes profiles by their display name. The display name must be unique for quality changes. This finds profiles that belong together in a group. @@ -147,18 +148,19 @@ class MachineNode(ContainerNode): return list(groups_by_name.values()) - ## Gets the preferred global quality node, going by the preferred quality - # type. - # - # If the preferred global quality is not in there, an arbitrary global - # quality is taken. - # If there are no global qualities, an empty quality is returned. def preferredGlobalQuality(self) -> "QualityNode": + """Gets the preferred global quality node, going by the preferred quality type. + + If the preferred global quality is not in there, an arbitrary global quality is taken. If there are no global + qualities, an empty quality is returned. + """ + return self.global_qualities.get(self.preferred_quality_type, next(iter(self.global_qualities.values()))) - ## (Re)loads all variants under this printer. @UM.FlameProfiler.profile def _loadAll(self) -> None: + """(Re)loads all variants under this printer.""" + container_registry = ContainerRegistry.getInstance() if not self.has_variants: self.variants["empty"] = VariantNode("empty_variant", machine = self) diff --git a/cura/Machines/MaterialGroup.py b/cura/Machines/MaterialGroup.py index e05647e674..2ff4b99c80 100644 --- a/cura/Machines/MaterialGroup.py +++ b/cura/Machines/MaterialGroup.py @@ -7,18 +7,21 @@ if TYPE_CHECKING: from cura.Machines.MaterialNode import MaterialNode -## A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile. -# The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For -# example: "generic_abs" is the root material (ID) of "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4", -# and "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4" are derived materials of "generic_abs". -# -# Using "generic_abs" as an example, the MaterialGroup for "generic_abs" will contain the following information: -# - name: "generic_abs", root_material_id -# - root_material_node: MaterialNode of "generic_abs" -# - derived_material_node_list: A list of MaterialNodes that are derived from "generic_abs", -# so "generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc. -# class MaterialGroup: + """A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile. + + The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For + example: "generic_abs" is the root material (ID) of "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4", + and "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4" are derived materials of "generic_abs". + + Using "generic_abs" as an example, the MaterialGroup for "generic_abs" will contain the following information: + - name: "generic_abs", root_material_id + - root_material_node: MaterialNode of "generic_abs" + - derived_material_node_list: A list of MaterialNodes that are derived from "generic_abs", so + "generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc. + + """ + __slots__ = ("name", "is_read_only", "root_material_node", "derived_material_node_list") def __init__(self, name: str, root_material_node: "MaterialNode") -> None: diff --git a/cura/Machines/MaterialNode.py b/cura/Machines/MaterialNode.py index dcd4adcfdb..c78c6aff03 100644 --- a/cura/Machines/MaterialNode.py +++ b/cura/Machines/MaterialNode.py @@ -15,10 +15,12 @@ if TYPE_CHECKING: from cura.Machines.VariantNode import VariantNode -## Represents a material in the container tree. -# -# Its subcontainers are quality profiles. class MaterialNode(ContainerNode): + """Represents a material in the container tree. + + Its subcontainers are quality profiles. + """ + def __init__(self, container_id: str, variant: "VariantNode") -> None: super().__init__(container_id) self.variant = variant @@ -34,16 +36,16 @@ class MaterialNode(ContainerNode): container_registry.containerRemoved.connect(self._onRemoved) container_registry.containerMetaDataChanged.connect(self._onMetadataChanged) - ## Finds the preferred quality for this printer with this material and this - # variant loaded. - # - # If the preferred quality is not available, an arbitrary quality is - # returned. If there is a configuration mistake (like a typo in the - # preferred quality) this returns a random available quality. If there are - # no available qualities, this will return the empty quality node. - # \return The node for the preferred quality, or any arbitrary quality if - # there is no match. def preferredQuality(self) -> QualityNode: + """Finds the preferred quality for this printer with this material and this variant loaded. + + If the preferred quality is not available, an arbitrary quality is returned. If there is a configuration + mistake (like a typo in the preferred quality) this returns a random available quality. If there are no + available qualities, this will return the empty quality node. + + :return: The node for the preferred quality, or any arbitrary quality if there is no match. + """ + for quality_id, quality_node in self.qualities.items(): if self.variant.machine.preferred_quality_type == quality_node.quality_type: return quality_node @@ -107,10 +109,13 @@ class MaterialNode(ContainerNode): if not self.qualities: self.qualities["empty_quality"] = QualityNode("empty_quality", parent = self) - ## Triggered when any container is removed, but only handles it when the - # container is removed that this node represents. - # \param container The container that was allegedly removed. def _onRemoved(self, container: ContainerInterface) -> None: + """Triggered when any container is removed, but only handles it when the container is removed that this node + represents. + + :param container: The container that was allegedly removed. + """ + if container.getId() == self.container_id: # Remove myself from my parent. if self.base_file in self.variant.materials: @@ -119,13 +124,15 @@ class MaterialNode(ContainerNode): self.variant.materials["empty_material"] = MaterialNode("empty_material", variant = self.variant) self.materialChanged.emit(self) - ## Triggered when any metadata changed in any container, but only handles - # it when the metadata of this node is changed. - # \param container The container whose metadata changed. - # \param kwargs Key-word arguments provided when changing the metadata. - # These are ignored. As far as I know they are never provided to this - # call. def _onMetadataChanged(self, container: ContainerInterface, **kwargs: Any) -> None: + """Triggered when any metadata changed in any container, but only handles it when the metadata of this node is + changed. + + :param container: The container whose metadata changed. + :param kwargs: Key-word arguments provided when changing the metadata. These are ignored. As far as I know they + are never provided to this call. + """ + if container.getId() != self.container_id: return diff --git a/cura/Machines/Models/BaseMaterialsModel.py b/cura/Machines/Models/BaseMaterialsModel.py index 5e672faa12..776d540867 100644 --- a/cura/Machines/Models/BaseMaterialsModel.py +++ b/cura/Machines/Models/BaseMaterialsModel.py @@ -13,11 +13,13 @@ from cura.Machines.ContainerTree import ContainerTree from cura.Machines.MaterialNode import MaterialNode from cura.Settings.CuraContainerRegistry import CuraContainerRegistry -## This is the base model class for GenericMaterialsModel and MaterialBrandsModel. -# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately. -# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top -# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu class BaseMaterialsModel(ListModel): + """This is the base model class for GenericMaterialsModel and MaterialBrandsModel. + + Those 2 models are used by the material drop down menu to show generic materials and branded materials + separately. The extruder position defined here is being used to bound a menu to the correct extruder. This is + used in the top bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu + """ extruderPositionChanged = pyqtSignal() enabledChanged = pyqtSignal() @@ -121,10 +123,13 @@ class BaseMaterialsModel(ListModel): def enabled(self): return self._enabled - ## Triggered when a list of materials changed somewhere in the container - # tree. This change may trigger an _update() call when the materials - # changed for the configuration that this model is looking for. def _materialsListChanged(self, material: MaterialNode) -> None: + """Triggered when a list of materials changed somewhere in the container + + tree. This change may trigger an _update() call when the materials changed for the configuration that this + model is looking for. + """ + if self._extruder_stack is None: return if material.variant.container_id != self._extruder_stack.variant.getId(): @@ -136,23 +141,25 @@ class BaseMaterialsModel(ListModel): return self._onChanged() - ## Triggered when the list of favorite materials is changed. def _favoritesChanged(self, material_base_file: str) -> None: + """Triggered when the list of favorite materials is changed.""" + if material_base_file in self._available_materials: self._onChanged() - ## This is an abstract method that needs to be implemented by the specific - # models themselves. def _update(self): + """This is an abstract method that needs to be implemented by the specific models themselves. """ + self._favorite_ids = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";")) # Update the available materials (ContainerNode) for the current active machine and extruder setup. global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() - if not global_stack.hasMaterials: + if not global_stack or not global_stack.hasMaterials: return # There are no materials for this machine, so nothing to do. - extruder_stack = global_stack.extruders.get(str(self._extruder_position)) - if not extruder_stack: + extruder_list = global_stack.extruderList + if self._extruder_position > len(extruder_list): return + extruder_stack = extruder_list[self._extruder_position] nozzle_name = extruder_stack.variant.getName() machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()] if nozzle_name not in machine_node.variants: @@ -163,23 +170,23 @@ class BaseMaterialsModel(ListModel): approximate_material_diameter = extruder_stack.getApproximateMaterialDiameter() self._available_materials = {key: material for key, material in materials.items() if float(material.getMetaDataEntry("approximate_diameter", -1)) == approximate_material_diameter} - ## This method is used by all material models in the beginning of the - # _update() method in order to prevent errors. It's the same in all models - # so it's placed here for easy access. def _canUpdate(self): + """This method is used by all material models in the beginning of the _update() method in order to prevent + errors. It's the same in all models so it's placed here for easy access. """ + global_stack = self._machine_manager.activeMachine if global_stack is None or not self._enabled: return False - extruder_position = str(self._extruder_position) - if extruder_position not in global_stack.extruders: + if self._extruder_position >= len(global_stack.extruderList): return False return True - ## This is another convenience function which is shared by all material - # models so it's put here to avoid having so much duplicated code. def _createMaterialItem(self, root_material_id, container_node): + """This is another convenience function which is shared by all material models so it's put here to avoid having + so much duplicated code. """ + metadata_list = CuraContainerRegistry.getInstance().findContainersMetadata(id = container_node.container_id) if not metadata_list: return None diff --git a/cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py b/cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py index 1ab7e21700..ce4b87da2b 100644 --- a/cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py +++ b/cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py @@ -14,9 +14,8 @@ if TYPE_CHECKING: from UM.Settings.Interfaces import ContainerInterface -## This model is used for the custom profile items in the profile drop down -# menu. class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel): + """This model is used for the custom profile items in the profile drop down menu.""" def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) diff --git a/cura/Machines/Models/DiscoveredCloudPrintersModel.py b/cura/Machines/Models/DiscoveredCloudPrintersModel.py index 23dcba6de7..692ed49593 100644 --- a/cura/Machines/Models/DiscoveredCloudPrintersModel.py +++ b/cura/Machines/Models/DiscoveredCloudPrintersModel.py @@ -9,9 +9,9 @@ if TYPE_CHECKING: class DiscoveredCloudPrintersModel(ListModel): - """ - Model used to inform the application about newly added cloud printers, which are discovered from the user's account - """ + """Model used to inform the application about newly added cloud printers, which are discovered from the user's + account """ + DeviceKeyRole = Qt.UserRole + 1 DeviceNameRole = Qt.UserRole + 2 DeviceTypeRole = Qt.UserRole + 3 @@ -31,18 +31,24 @@ class DiscoveredCloudPrintersModel(ListModel): self._application = application # type: CuraApplication def addDiscoveredCloudPrinters(self, new_devices: List[Dict[str, str]]) -> None: - """ - Adds all the newly discovered cloud printers into the DiscoveredCloudPrintersModel. + """Adds all the newly discovered cloud printers into the DiscoveredCloudPrintersModel. + + Example new_devices entry: + + .. code-block:: python - :param new_devices: List of dictionaries which contain information about added cloud printers. Example: { "key": "YjW8pwGYcaUvaa0YgVyWeFkX3z", "name": "NG 001", "machine_type": "Ultimaker S5", "firmware_version": "5.5.12.202001" } + + :param new_devices: List of dictionaries which contain information about added cloud printers. + :return: None """ + self._discovered_cloud_printers_list.extend(new_devices) self._update() @@ -51,21 +57,21 @@ class DiscoveredCloudPrintersModel(ListModel): @pyqtSlot() def clear(self) -> None: - """ - Clears the contents of the DiscoveredCloudPrintersModel. + """Clears the contents of the DiscoveredCloudPrintersModel. :return: None """ + self._discovered_cloud_printers_list = [] self._update() self.cloudPrintersDetectedChanged.emit(False) def _update(self) -> None: - """ - Sorts the newly discovered cloud printers by name and then updates the ListModel. + """Sorts the newly discovered cloud printers by name and then updates the ListModel. :return: None """ + items = self._discovered_cloud_printers_list[:] items.sort(key = lambda k: k["name"]) self.setItems(items) diff --git a/cura/Machines/Models/DiscoveredPrintersModel.py b/cura/Machines/Models/DiscoveredPrintersModel.py index 6d1bbdb698..459ec4d795 100644 --- a/cura/Machines/Models/DiscoveredPrintersModel.py +++ b/cura/Machines/Models/DiscoveredPrintersModel.py @@ -115,12 +115,11 @@ class DiscoveredPrinter(QObject): return catalog.i18nc("@label", "Available networked printers") -# -# Discovered printers are all the printers that were found on the network, which provide a more convenient way -# to add networked printers (Plugin finds a bunch of printers, user can select one from the list, plugin can then -# add that printer to Cura as the active one). -# class DiscoveredPrintersModel(QObject): + """Discovered printers are all the printers that were found on the network, which provide a more convenient way to + add networked printers (Plugin finds a bunch of printers, user can select one from the list, plugin can then add + that printer to Cura as the active one). + """ def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None: super().__init__(parent) @@ -129,6 +128,7 @@ class DiscoveredPrintersModel(QObject): self._discovered_printer_by_ip_dict = dict() # type: Dict[str, DiscoveredPrinter] self._plugin_for_manual_device = None # type: Optional[OutputDevicePlugin] + self._network_plugin_queue = [] # type: List[OutputDevicePlugin] self._manual_device_address = "" self._manual_device_request_timeout_in_seconds = 5 # timeout for adding a manual device in seconds @@ -153,20 +153,25 @@ class DiscoveredPrintersModel(QObject): all_plugins_dict = self._application.getOutputDeviceManager().getAllOutputDevicePlugins() - can_add_manual_plugins = [item for item in filter( + self._network_plugin_queue = [item for item in filter( lambda plugin_item: plugin_item.canAddManualDevice(address) in priority_order, all_plugins_dict.values())] - if not can_add_manual_plugins: + if not self._network_plugin_queue: Logger.log("d", "Could not find a plugin to accept adding %s manually via address.", address) return - plugin = max(can_add_manual_plugins, key = lambda p: priority_order.index(p.canAddManualDevice(address))) - self._plugin_for_manual_device = plugin - self._plugin_for_manual_device.addManualDevice(address, callback = self._onManualDeviceRequestFinished) - self._manual_device_address = address - self._manual_device_request_timer.start() - self.hasManualDeviceRequestInProgressChanged.emit() + self._attemptToAddManualDevice(address) + + def _attemptToAddManualDevice(self, address: str) -> None: + if self._network_plugin_queue: + self._plugin_for_manual_device = self._network_plugin_queue.pop() + Logger.log("d", "Network plugin %s: attempting to add manual device with address %s.", + self._plugin_for_manual_device.getId(), address) + self._plugin_for_manual_device.addManualDevice(address, callback=self._onManualDeviceRequestFinished) + self._manual_device_address = address + self._manual_device_request_timer.start() + self.hasManualDeviceRequestInProgressChanged.emit() @pyqtSlot() def cancelCurrentManualDeviceRequest(self) -> None: @@ -181,8 +186,11 @@ class DiscoveredPrintersModel(QObject): self.manualDeviceRequestFinished.emit(False) def _onManualRequestTimeout(self) -> None: - Logger.log("w", "Manual printer [%s] request timed out. Cancel the current request.", self._manual_device_address) + address = self._manual_device_address + Logger.log("w", "Manual printer [%s] request timed out. Cancel the current request.", address) self.cancelCurrentManualDeviceRequest() + if self._network_plugin_queue: + self._attemptToAddManualDevice(address) hasManualDeviceRequestInProgressChanged = pyqtSignal() @@ -198,11 +206,13 @@ class DiscoveredPrintersModel(QObject): self._manual_device_address = "" self.hasManualDeviceRequestInProgressChanged.emit() self.manualDeviceRequestFinished.emit(success) + if not success and self._network_plugin_queue: + self._attemptToAddManualDevice(address) @pyqtProperty("QVariantMap", notify = discoveredPrintersChanged) def discoveredPrintersByAddress(self) -> Dict[str, DiscoveredPrinter]: return self._discovered_printer_by_ip_dict - + @pyqtProperty("QVariantList", notify = discoveredPrintersChanged) def discoveredPrinters(self) -> List["DiscoveredPrinter"]: item_list = list( @@ -254,8 +264,14 @@ class DiscoveredPrintersModel(QObject): del self._discovered_printer_by_ip_dict[ip_address] self.discoveredPrintersChanged.emit() - # A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer. - # This function invokes the given discovered printer's "create_callback" to do this. + @pyqtSlot("QVariant") def createMachineFromDiscoveredPrinter(self, discovered_printer: "DiscoveredPrinter") -> None: + """A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer. + + This function invokes the given discovered printer's "create_callback" to do this + + :param discovered_printer: + """ + discovered_printer.create_callback(discovered_printer.getKey()) diff --git a/cura/Machines/Models/ExtrudersModel.py b/cura/Machines/Models/ExtrudersModel.py index 9eee7f5f9e..98865ed37e 100644 --- a/cura/Machines/Models/ExtrudersModel.py +++ b/cura/Machines/Models/ExtrudersModel.py @@ -15,27 +15,27 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## Model that holds extruders. -# -# This model is designed for use by any list of extruders, but specifically -# intended for drop-down lists of the current machine's extruders in place of -# settings. class ExtrudersModel(ListModel): + """Model that holds extruders. + + This model is designed for use by any list of extruders, but specifically intended for drop-down lists of the + current machine's extruders in place of settings. + """ + # The ID of the container stack for the extruder. IdRole = Qt.UserRole + 1 - ## Human-readable name of the extruder. NameRole = Qt.UserRole + 2 + """Human-readable name of the extruder.""" - ## Colour of the material loaded in the extruder. ColorRole = Qt.UserRole + 3 + """Colour of the material loaded in the extruder.""" - ## Index of the extruder, which is also the value of the setting itself. - # - # An index of 0 indicates the first extruder, an index of 1 the second - # one, and so on. This is the value that will be saved in instance - # containers. IndexRole = Qt.UserRole + 4 + """Index of the extruder, which is also the value of the setting itself. + + An index of 0 indicates the first extruder, an index of 1 the second one, and so on. This is the value that will + be saved in instance containers. """ # The ID of the definition of the extruder. DefinitionRole = Qt.UserRole + 5 @@ -50,18 +50,18 @@ class ExtrudersModel(ListModel): MaterialBrandRole = Qt.UserRole + 9 ColorNameRole = Qt.UserRole + 10 - ## Is the extruder enabled? EnabledRole = Qt.UserRole + 11 + """Is the extruder enabled?""" - ## List of colours to display if there is no material or the material has no known - # colour. defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"] + """List of colours to display if there is no material or the material has no known colour. """ - ## Initialises the extruders model, defining the roles and listening for - # changes in the data. - # - # \param parent Parent QtObject of this list. def __init__(self, parent = None): + """Initialises the extruders model, defining the roles and listening for changes in the data. + + :param parent: Parent QtObject of this list. + """ + super().__init__(parent) self.addRoleName(self.IdRole, "id") @@ -101,14 +101,15 @@ class ExtrudersModel(ListModel): def addOptionalExtruder(self): return self._add_optional_extruder - ## Links to the stack-changed signal of the new extruders when an extruder - # is swapped out or added in the current machine. - # - # \param machine_id The machine for which the extruders changed. This is - # filled by the ExtruderManager.extrudersChanged signal when coming from - # that signal. Application.globalContainerStackChanged doesn't fill this - # signal; it's assumed to be the current printer in that case. def _extrudersChanged(self, machine_id = None): + """Links to the stack-changed signal of the new extruders when an extruder is swapped out or added in the + current machine. + + :param machine_id: The machine for which the extruders changed. This is filled by the + ExtruderManager.extrudersChanged signal when coming from that signal. Application.globalContainerStackChanged + doesn't fill this signal; it's assumed to be the current printer in that case. + """ + machine_manager = Application.getInstance().getMachineManager() if machine_id is not None: if machine_manager.activeMachine is None: @@ -146,11 +147,13 @@ class ExtrudersModel(ListModel): def _updateExtruders(self): self._update_extruder_timer.start() - ## Update the list of extruders. - # - # This should be called whenever the list of extruders changes. @UM.FlameProfiler.profile def __updateExtruders(self): + """Update the list of extruders. + + This should be called whenever the list of extruders changes. + """ + extruders_changed = False if self.count != 0: diff --git a/cura/Machines/Models/FavoriteMaterialsModel.py b/cura/Machines/Models/FavoriteMaterialsModel.py index 6b8f0e8e56..203888d6fb 100644 --- a/cura/Machines/Models/FavoriteMaterialsModel.py +++ b/cura/Machines/Models/FavoriteMaterialsModel.py @@ -4,16 +4,17 @@ from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel import cura.CuraApplication # To listen to changes to the preferences. -## Model that shows the list of favorite materials. class FavoriteMaterialsModel(BaseMaterialsModel): + """Model that shows the list of favorite materials.""" + def __init__(self, parent = None): super().__init__(parent) cura.CuraApplication.CuraApplication.getInstance().getPreferences().preferenceChanged.connect(self._onFavoritesChanged) self._onChanged() - ## Triggered when any preference changes, but only handles it when the list - # of favourites is changed. def _onFavoritesChanged(self, preference_key: str) -> None: + """Triggered when any preference changes, but only handles it when the list of favourites is changed. """ + if preference_key != "cura/favorite_materials": return self._onChanged() diff --git a/cura/Machines/Models/FirstStartMachineActionsModel.py b/cura/Machines/Models/FirstStartMachineActionsModel.py index 92caed7b12..7d83f0bff2 100644 --- a/cura/Machines/Models/FirstStartMachineActionsModel.py +++ b/cura/Machines/Models/FirstStartMachineActionsModel.py @@ -11,13 +11,13 @@ if TYPE_CHECKING: from cura.CuraApplication import CuraApplication -# -# This model holds all first-start machine actions for the currently active machine. It has 2 roles: -# - title : the title/name of the action -# - content : the QObject of the QML content of the action -# - action : the MachineAction object itself -# class FirstStartMachineActionsModel(ListModel): + """This model holds all first-start machine actions for the currently active machine. It has 2 roles: + + - title : the title/name of the action + - content : the QObject of the QML content of the action + - action : the MachineAction object itself + """ TitleRole = Qt.UserRole + 1 ContentRole = Qt.UserRole + 2 @@ -73,9 +73,10 @@ class FirstStartMachineActionsModel(ListModel): self._current_action_index += 1 self.currentActionIndexChanged.emit() - # Resets the current action index to 0 so the wizard panel can show actions from the beginning. @pyqtSlot() def reset(self) -> None: + """Resets the current action index to 0 so the wizard panel can show actions from the beginning.""" + self._current_action_index = 0 self.currentActionIndexChanged.emit() diff --git a/cura/Machines/Models/GlobalStacksModel.py b/cura/Machines/Models/GlobalStacksModel.py index 9db4ffe6db..6d091659a8 100644 --- a/cura/Machines/Models/GlobalStacksModel.py +++ b/cura/Machines/Models/GlobalStacksModel.py @@ -19,6 +19,7 @@ class GlobalStacksModel(ListModel): ConnectionTypeRole = Qt.UserRole + 4 MetaDataRole = Qt.UserRole + 5 DiscoverySourceRole = Qt.UserRole + 6 # For separating local and remote printers in the machine management page + RemovalWarningRole = Qt.UserRole + 7 def __init__(self, parent = None) -> None: super().__init__(parent) @@ -42,8 +43,9 @@ class GlobalStacksModel(ListModel): CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged) self._updateDelayed() - ## Handler for container added/removed events from registry def _onContainerChanged(self, container) -> None: + """Handler for container added/removed events from registry""" + # We only need to update when the added / removed container GlobalStack if isinstance(container, GlobalStack): self._updateDelayed() @@ -65,13 +67,21 @@ class GlobalStacksModel(ListModel): if parseBool(container_stack.getMetaDataEntry("hidden", False)): continue - section_name = "Network enabled printers" if has_remote_connection else "Local printers" + 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) - items.append({"name": container_stack.getMetaDataEntry("group_name", container_stack.getName()), + default_removal_warning = self._catalog.i18nc( + "@label ({} is object name)", + "Are you sure you wish to remove {}? This cannot be undone!", device_name + ) + removal_warning = container_stack.getMetaDataEntry("removal_warning", default_removal_warning) + + items.append({"name": device_name, "id": container_stack.getId(), "hasRemoteConnection": has_remote_connection, "metadata": container_stack.getMetaData().copy(), - "discoverySource": section_name}) - items.sort(key = lambda i: (not i["hasRemoteConnection"], i["name"])) + "discoverySource": section_name, + "removalWarning": removal_warning}) + items.sort(key=lambda i: (not i["hasRemoteConnection"], i["name"])) self.setItems(items) diff --git a/cura/Machines/Models/IntentCategoryModel.py b/cura/Machines/Models/IntentCategoryModel.py index 427e60ec0c..09a71b8ed6 100644 --- a/cura/Machines/Models/IntentCategoryModel.py +++ b/cura/Machines/Models/IntentCategoryModel.py @@ -18,9 +18,9 @@ from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") -## Lists the intent categories that are available for the current printer -# configuration. class IntentCategoryModel(ListModel): + """Lists the intent categories that are available for the current printer configuration. """ + NameRole = Qt.UserRole + 1 IntentCategoryRole = Qt.UserRole + 2 WeightRole = Qt.UserRole + 3 @@ -31,10 +31,12 @@ class IntentCategoryModel(ListModel): _translations = collections.OrderedDict() # type: "collections.OrderedDict[str,Dict[str,Optional[str]]]" - # Translations to user-visible string. Ordered by weight. - # TODO: Create a solution for this name and weight to be used dynamically. @classmethod def _get_translations(cls): + """Translations to user-visible string. Ordered by weight. + + TODO: Create a solution for this name and weight to be used dynamically. + """ if len(cls._translations) == 0: cls._translations["default"] = { "name": catalog.i18nc("@label", "Default") @@ -53,9 +55,12 @@ class IntentCategoryModel(ListModel): } return cls._translations - ## Creates a new model for a certain intent category. - # \param The category to list the intent profiles for. def __init__(self, intent_category: str) -> None: + """Creates a new model for a certain intent category. + + :param intent_category: category to list the intent profiles for. + """ + super().__init__() self._intent_category = intent_category @@ -84,16 +89,18 @@ class IntentCategoryModel(ListModel): self.update() - ## Updates the list of intents if an intent profile was added or removed. def _onContainerChange(self, container: "ContainerInterface") -> None: + """Updates the list of intents if an intent profile was added or removed.""" + if container.getMetaDataEntry("type") == "intent": self.update() def update(self): self._update_timer.start() - ## Updates the list of intents. def _update(self) -> None: + """Updates the list of intents.""" + available_categories = IntentManager.getInstance().currentAvailableIntentCategories() result = [] for category in available_categories: @@ -109,9 +116,9 @@ class IntentCategoryModel(ListModel): result.sort(key = lambda k: k["weight"]) self.setItems(result) - ## Get a display value for a category. - ## for categories and keys @staticmethod def translation(category: str, key: str, default: Optional[str] = None): + """Get a display value for a category.for categories and keys""" + display_strings = IntentCategoryModel._get_translations().get(category, {}) return display_strings.get(key, default) diff --git a/cura/Machines/Models/IntentModel.py b/cura/Machines/Models/IntentModel.py index 951be7ab2d..0ec7e268f0 100644 --- a/cura/Machines/Models/IntentModel.py +++ b/cura/Machines/Models/IntentModel.py @@ -98,8 +98,9 @@ class IntentModel(ListModel): new_items = sorted(new_items, key = lambda x: x["layer_height"]) self.setItems(new_items) - ## Get the active materials for all extruders. No duplicates will be returned def _getActiveMaterials(self) -> Set["MaterialNode"]: + """Get the active materials for all extruders. No duplicates will be returned""" + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return set() diff --git a/cura/Machines/Models/MaterialManagementModel.py b/cura/Machines/Models/MaterialManagementModel.py index f00b81e987..4a696ec974 100644 --- a/cura/Machines/Models/MaterialManagementModel.py +++ b/cura/Machines/Models/MaterialManagementModel.py @@ -19,28 +19,31 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## Proxy class to the materials page in the preferences. -# -# This class handles the actions in that page, such as creating new materials, -# renaming them, etc. class MaterialManagementModel(QObject): - ## Triggered when a favorite is added or removed. - # \param The base file of the material is provided as parameter when this - # emits. - favoritesChanged = pyqtSignal(str) + """Proxy class to the materials page in the preferences. + + This class handles the actions in that page, such as creating new materials, renaming them, etc. + """ + + favoritesChanged = pyqtSignal(str) + """Triggered when a favorite is added or removed. + + :param The base file of the material is provided as parameter when this emits + """ - ## Can a certain material be deleted, or is it still in use in one of the - # container stacks anywhere? - # - # We forbid the user from deleting a material if it's in use in any stack. - # Deleting it while it's in use can lead to corrupted stacks. In the - # future we might enable this functionality again (deleting the material - # from those stacks) but for now it is easier to prevent the user from - # doing this. - # \param material_node The ContainerTree node of the material to check. - # \return Whether or not the material can be removed. @pyqtSlot("QVariant", result = bool) def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool: + """Can a certain material be deleted, or is it still in use in one of the container stacks anywhere? + + We forbid the user from deleting a material if it's in use in any stack. Deleting it while it's in use can + lead to corrupted stacks. In the future we might enable this functionality again (deleting the material from + those stacks) but for now it is easier to prevent the user from doing this. + + :param material_node: The ContainerTree node of the material to check. + + :return: Whether or not the material can be removed. + """ + container_registry = CuraContainerRegistry.getInstance() ids_to_remove = {metadata.get("id", "") for metadata in container_registry.findInstanceContainersMetadata(base_file = material_node.base_file)} for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"): @@ -48,11 +51,14 @@ class MaterialManagementModel(QObject): return False return True - ## Change the user-visible name of a material. - # \param material_node The ContainerTree node of the material to rename. - # \param name The new name for the material. @pyqtSlot("QVariant", str) def setMaterialName(self, material_node: "MaterialNode", name: str) -> None: + """Change the user-visible name of a material. + + :param material_node: The ContainerTree node of the material to rename. + :param name: The new name for the material. + """ + container_registry = CuraContainerRegistry.getInstance() root_material_id = material_node.base_file if container_registry.isReadOnly(root_material_id): @@ -60,18 +66,20 @@ class MaterialManagementModel(QObject): return return container_registry.findContainers(id = root_material_id)[0].setName(name) - ## Deletes a material from Cura. - # - # This function does not do any safety checking any more. Please call this - # function only if: - # - The material is not read-only. - # - The material is not used in any stacks. - # If the material was not lazy-loaded yet, this will fully load the - # container. When removing this material node, all other materials with - # the same base fill will also be removed. - # \param material_node The material to remove. @pyqtSlot("QVariant") def removeMaterial(self, material_node: "MaterialNode") -> None: + """Deletes a material from Cura. + + This function does not do any safety checking any more. Please call this function only if: + - The material is not read-only. + - The material is not used in any stacks. + + If the material was not lazy-loaded yet, this will fully load the container. When removing this material + node, all other materials with the same base fill will also be removed. + + :param material_node: The material to remove. + """ + container_registry = CuraContainerRegistry.getInstance() materials_this_base_file = container_registry.findContainersMetadata(base_file = material_node.base_file) @@ -89,17 +97,19 @@ class MaterialManagementModel(QObject): for material_metadata in materials_this_base_file: container_registry.removeContainer(material_metadata["id"]) - ## Creates a duplicate of a material with the same GUID and base_file - # metadata. - # \param base_file: The base file of the material to duplicate. - # \param new_base_id A new material ID for the base material. The IDs of - # the submaterials will be based off this one. If not provided, a material - # ID will be generated automatically. - # \param new_metadata Metadata for the new material. If not provided, this - # will be duplicated from the original material. - # \return The root material ID of the duplicate material. def duplicateMaterialByBaseFile(self, base_file: str, new_base_id: Optional[str] = None, new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]: + """Creates a duplicate of a material with the same GUID and base_file metadata + + :param base_file: The base file of the material to duplicate. + :param new_base_id: A new material ID for the base material. The IDs of the submaterials will be based off this + one. If not provided, a material ID will be generated automatically. + :param new_metadata: Metadata for the new material. If not provided, this will be duplicated from the original + material. + + :return: The root material ID of the duplicate material. + """ + container_registry = CuraContainerRegistry.getInstance() root_materials = container_registry.findContainers(id = base_file) @@ -171,29 +181,32 @@ class MaterialManagementModel(QObject): return new_base_id - ## Creates a duplicate of a material with the same GUID and base_file - # metadata. - # \param material_node The node representing the material to duplicate. - # \param new_base_id A new material ID for the base material. The IDs of - # the submaterials will be based off this one. If not provided, a material - # ID will be generated automatically. - # \param new_metadata Metadata for the new material. If not provided, this - # will be duplicated from the original material. - # \return The root material ID of the duplicate material. @pyqtSlot("QVariant", result = str) def duplicateMaterial(self, material_node: "MaterialNode", new_base_id: Optional[str] = None, new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]: + """Creates a duplicate of a material with the same GUID and base_file metadata + + :param material_node: The node representing the material to duplicate. + :param new_base_id: A new material ID for the base material. The IDs of the submaterials will be based off this + one. If not provided, a material ID will be generated automatically. + :param new_metadata: Metadata for the new material. If not provided, this will be duplicated from the original + material. + + :return: The root material ID of the duplicate material. + """ return self.duplicateMaterialByBaseFile(material_node.base_file, new_base_id, new_metadata) - ## Create a new material by cloning the preferred material for the current - # material diameter and generate a new GUID. - # - # The material type is explicitly left to be the one from the preferred - # material, since this allows the user to still have SOME profiles to work - # with. - # \return The ID of the newly created material. @pyqtSlot(result = str) def createMaterial(self) -> str: + """Create a new material by cloning the preferred material for the current material diameter and generate a new + GUID. + + The material type is explicitly left to be the one from the preferred material, since this allows the user to + still have SOME profiles to work with. + + :return: The ID of the newly created material. + """ + # Ensure all settings are saved. application = cura.CuraApplication.CuraApplication.getInstance() application.saveSettings() @@ -218,10 +231,13 @@ class MaterialManagementModel(QObject): self.duplicateMaterial(preferred_material_node, new_base_id = new_id, new_metadata = new_metadata) return new_id - ## Adds a certain material to the favorite materials. - # \param material_base_file The base file of the material to add. @pyqtSlot(str) def addFavorite(self, material_base_file: str) -> None: + """Adds a certain material to the favorite materials. + + :param material_base_file: The base file of the material to add. + """ + application = cura.CuraApplication.CuraApplication.getInstance() favorites = application.getPreferences().getValue("cura/favorite_materials").split(";") if material_base_file not in favorites: @@ -230,11 +246,13 @@ class MaterialManagementModel(QObject): application.saveSettings() self.favoritesChanged.emit(material_base_file) - ## Removes a certain material from the favorite materials. - # - # If the material was not in the favorite materials, nothing happens. @pyqtSlot(str) def removeFavorite(self, material_base_file: str) -> None: + """Removes a certain material from the favorite materials. + + If the material was not in the favorite materials, nothing happens. + """ + application = cura.CuraApplication.CuraApplication.getInstance() favorites = application.getPreferences().getValue("cura/favorite_materials").split(";") try: diff --git a/cura/Machines/Models/MultiBuildPlateModel.py b/cura/Machines/Models/MultiBuildPlateModel.py index add960a545..8e2f086e3b 100644 --- a/cura/Machines/Models/MultiBuildPlateModel.py +++ b/cura/Machines/Models/MultiBuildPlateModel.py @@ -9,11 +9,11 @@ from UM.Scene.Selection import Selection from UM.Qt.ListModel import ListModel -# -# This is the model for multi build plate feature. -# This has nothing to do with the build plate types you can choose on the sidebar for a machine. -# class MultiBuildPlateModel(ListModel): + """This is the model for multi build plate feature. + + This has nothing to do with the build plate types you can choose on the sidebar for a machine. + """ maxBuildPlateChanged = pyqtSignal() activeBuildPlateChanged = pyqtSignal() @@ -39,9 +39,10 @@ class MultiBuildPlateModel(ListModel): self._max_build_plate = max_build_plate self.maxBuildPlateChanged.emit() - ## Return the highest build plate number @pyqtProperty(int, notify = maxBuildPlateChanged) def maxBuildPlate(self): + """Return the highest build plate number""" + return self._max_build_plate def setActiveBuildPlate(self, nr): diff --git a/cura/Machines/Models/QualityManagementModel.py b/cura/Machines/Models/QualityManagementModel.py index 74dc8649d0..663a49dbc1 100644 --- a/cura/Machines/Models/QualityManagementModel.py +++ b/cura/Machines/Models/QualityManagementModel.py @@ -26,10 +26,9 @@ if TYPE_CHECKING: from cura.Settings.GlobalStack import GlobalStack -# -# This the QML model for the quality management page. -# class QualityManagementModel(ListModel): + """This the QML model for the quality management page.""" + NameRole = Qt.UserRole + 1 IsReadOnlyRole = Qt.UserRole + 2 QualityGroupRole = Qt.UserRole + 3 @@ -74,11 +73,13 @@ class QualityManagementModel(ListModel): def _onChange(self) -> None: self._update_timer.start() - ## Deletes a custom profile. It will be gone forever. - # \param quality_changes_group The quality changes group representing the - # profile to delete. @pyqtSlot(QObject) def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None: + """Deletes a custom profile. It will be gone forever. + + :param quality_changes_group: The quality changes group representing the profile to delete. + """ + Logger.log("i", "Removing quality changes group {group_name}".format(group_name = quality_changes_group.name)) removed_quality_changes_ids = set() container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry() @@ -95,16 +96,19 @@ class QualityManagementModel(ListModel): if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids: extruder_stack.qualityChanges = empty_quality_changes_container - ## Rename a custom profile. - # - # Because the names must be unique, the new name may not actually become - # the name that was given. The actual name is returned by this function. - # \param quality_changes_group The custom profile that must be renamed. - # \param new_name The desired name for the profile. - # \return The actual new name of the profile, after making the name - # unique. @pyqtSlot(QObject, str, result = str) def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str: + """Rename a custom profile. + + Because the names must be unique, the new name may not actually become the name that was given. The actual + name is returned by this function. + + :param quality_changes_group: The custom profile that must be renamed. + :param new_name: The desired name for the profile. + + :return: The actual new name of the profile, after making the name unique. + """ + Logger.log("i", "Renaming QualityChangesGroup {old_name} to {new_name}.".format(old_name = quality_changes_group.name, new_name = new_name)) if new_name == quality_changes_group.name: Logger.log("i", "QualityChangesGroup name {name} unchanged.".format(name = quality_changes_group.name)) @@ -138,13 +142,16 @@ class QualityManagementModel(ListModel): return new_name - ## Duplicates a given quality profile OR quality changes profile. - # \param new_name The desired name of the new profile. This will be made - # unique, so it might end up with a different name. - # \param quality_model_item The item of this model to duplicate, as - # dictionary. See the descriptions of the roles of this list model. @pyqtSlot(str, "QVariantMap") def duplicateQualityChanges(self, new_name: str, quality_model_item: Dict[str, Any]) -> None: + """Duplicates a given quality profile OR quality changes profile. + + :param new_name: The desired name of the new profile. This will be made unique, so it might end up with a + different name. + :param quality_model_item: The item of this model to duplicate, as dictionary. See the descriptions of the + roles of this list model. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if not global_stack: Logger.log("i", "No active global stack, cannot duplicate quality (changes) profile.") @@ -170,18 +177,18 @@ class QualityManagementModel(ListModel): new_id = container_registry.uniqueName(container.getId()) container_registry.addContainer(container.duplicate(new_id, new_name)) - ## Create quality changes containers from the user containers in the active - # stacks. - # - # This will go through the global and extruder stacks and create - # quality_changes containers from the user containers in each stack. These - # then replace the quality_changes containers in the stack and clear the - # user settings. - # \param base_name The new name for the quality changes profile. The final - # name of the profile might be different from this, because it needs to be - # made unique. @pyqtSlot(str) def createQualityChanges(self, base_name: str) -> None: + """Create quality changes containers from the user containers in the active stacks. + + This will go through the global and extruder stacks and create quality_changes containers from the user + containers in each stack. These then replace the quality_changes containers in the stack and clear the user + settings. + + :param base_name: The new name for the quality changes profile. The final name of the profile might be + different from this, because it needs to be made unique. + """ + machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager() global_stack = machine_manager.activeMachine @@ -201,7 +208,7 @@ class QualityManagementModel(ListModel): # Go through the active stacks and create quality_changes containers from the user containers. container_manager = ContainerManager.getInstance() - stack_list = [global_stack] + list(global_stack.extruders.values()) + stack_list = [global_stack] + global_stack.extruderList for stack in stack_list: quality_container = stack.quality quality_changes_container = stack.qualityChanges @@ -220,14 +227,16 @@ class QualityManagementModel(ListModel): container_registry.addContainer(new_changes) - ## Create a quality changes container with the given set-up. - # \param quality_type The quality type of the new container. - # \param intent_category The intent category of the new container. - # \param new_name The name of the container. This name must be unique. - # \param machine The global stack to create the profile for. - # \param extruder_stack The extruder stack to create the profile for. If - # not provided, only a global container will be created. def _createQualityChanges(self, quality_type: str, intent_category: Optional[str], new_name: str, machine: "GlobalStack", extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer": + """Create a quality changes container with the given set-up. + + :param quality_type: The quality type of the new container. + :param intent_category: The intent category of the new container. + :param new_name: The name of the container. This name must be unique. + :param machine: The global stack to create the profile for. + :param extruder_stack: The extruder stack to create the profile for. If not provided, only a global container will be created. + """ + container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry() base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId() new_id = base_id + "_" + new_name @@ -253,11 +262,13 @@ class QualityManagementModel(ListModel): quality_changes.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.getInstance().SettingVersion) return quality_changes - ## Triggered when any container changed. - # - # This filters the updates to the container manager: When it applies to - # the list of quality changes, we need to update our list. def _qualityChangesListChanged(self, container: "ContainerInterface") -> None: + """Triggered when any container changed. + + This filters the updates to the container manager: When it applies to the list of quality changes, we need to + update our list. + """ + if container.getMetaDataEntry("type") == "quality_changes": self._update() @@ -366,18 +377,19 @@ class QualityManagementModel(ListModel): self.setItems(item_list) - # TODO: Duplicated code here from InstanceContainersModel. Refactor and remove this later. - # - ## Gets a list of the possible file filters that the plugins have - # registered they can read or write. The convenience meta-filters - # "All Supported Types" and "All Files" are added when listing - # readers, but not when listing writers. - # - # \param io_type \type{str} name of the needed IO type - # \return A list of strings indicating file name filters for a file - # dialog. @pyqtSlot(str, result = "QVariantList") def getFileNameFilters(self, io_type): + """Gets a list of the possible file filters that the plugins have registered they can read or write. + + The convenience meta-filters "All Supported Types" and "All Files" are added when listing readers, + but not when listing writers. + + :param io_type: name of the needed IO type + :return: A list of strings indicating file name filters for a file dialog. + + TODO: Duplicated code here from InstanceContainersModel. Refactor and remove this later. + """ + from UM.i18n import i18nCatalog catalog = i18nCatalog("uranium") #TODO: This function should be in UM.Resources! @@ -394,9 +406,11 @@ class QualityManagementModel(ListModel): filters.append(catalog.i18nc("@item:inlistbox", "All Files (*)")) # Also allow arbitrary files, if the user so prefers. return filters - ## Gets a list of profile reader or writer plugins - # \return List of tuples of (plugin_id, meta_data). def _getIOPlugins(self, io_type): + """Gets a list of profile reader or writer plugins + + :return: List of tuples of (plugin_id, meta_data). + """ from UM.PluginRegistry import PluginRegistry pr = PluginRegistry.getInstance() active_plugin_ids = pr.getActivePlugins() diff --git a/cura/Machines/Models/QualityProfilesDropDownMenuModel.py b/cura/Machines/Models/QualityProfilesDropDownMenuModel.py index 3a79ceeaf1..7aa30c6f82 100644 --- a/cura/Machines/Models/QualityProfilesDropDownMenuModel.py +++ b/cura/Machines/Models/QualityProfilesDropDownMenuModel.py @@ -10,10 +10,9 @@ from cura.Machines.ContainerTree import ContainerTree from cura.Machines.Models.MachineModelUtils import fetchLayerHeight -# -# QML Model for all built-in quality profiles. This model is used for the drop-down quality menu. -# class QualityProfilesDropDownMenuModel(ListModel): + """QML Model for all built-in quality profiles. This model is used for the drop-down quality menu.""" + NameRole = Qt.UserRole + 1 QualityTypeRole = Qt.UserRole + 2 LayerHeightRole = Qt.UserRole + 3 diff --git a/cura/Machines/Models/QualitySettingsModel.py b/cura/Machines/Models/QualitySettingsModel.py index 8a956263e7..c88e103f3a 100644 --- a/cura/Machines/Models/QualitySettingsModel.py +++ b/cura/Machines/Models/QualitySettingsModel.py @@ -10,10 +10,9 @@ from UM.Qt.ListModel import ListModel from UM.Settings.ContainerRegistry import ContainerRegistry -# -# This model is used to show details settings of the selected quality in the quality management page. -# class QualitySettingsModel(ListModel): + """This model is used to show details settings of the selected quality in the quality management page.""" + KeyRole = Qt.UserRole + 1 LabelRole = Qt.UserRole + 2 UnitRole = Qt.UserRole + 3 @@ -101,7 +100,8 @@ class QualitySettingsModel(ListModel): # the settings in that quality_changes_group. if quality_changes_group is not None: container_registry = ContainerRegistry.getInstance() - global_containers = container_registry.findContainers(id = quality_changes_group.metadata_for_global["id"]) + metadata_for_global = quality_changes_group.metadata_for_global + global_containers = container_registry.findContainers(id = metadata_for_global["id"]) global_container = None if len(global_containers) == 0 else global_containers[0] extruders_containers = {pos: container_registry.findContainers(id = quality_changes_group.metadata_per_extruder[pos]["id"]) for pos in quality_changes_group.metadata_per_extruder} extruders_container = {pos: None if not containers else containers[0] for pos, containers in extruders_containers.items()} @@ -152,7 +152,7 @@ class QualitySettingsModel(ListModel): if self._selected_position == self.GLOBAL_STACK_POSITION: user_value = global_container_stack.userChanges.getProperty(definition.key, "value") else: - extruder_stack = global_container_stack.extruders[str(self._selected_position)] + extruder_stack = global_container_stack.extruderList[self._selected_position] user_value = extruder_stack.userChanges.getProperty(definition.key, "value") if profile_value is None and user_value is None: diff --git a/cura/Machines/QualityChangesGroup.py b/cura/Machines/QualityChangesGroup.py index 655060070b..668fff785a 100644 --- a/cura/Machines/QualityChangesGroup.py +++ b/cura/Machines/QualityChangesGroup.py @@ -6,12 +6,12 @@ from typing import Any, Dict, Optional from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal -## Data struct to group several quality changes instance containers together. -# -# Each group represents one "custom profile" as the user sees it, which -# contains an instance container for the global stack and one instance -# container per extruder. class QualityChangesGroup(QObject): + """Data struct to group several quality changes instance containers together. + + Each group represents one "custom profile" as the user sees it, which contains an instance container for the + global stack and one instance container per extruder. + """ def __init__(self, name: str, quality_type: str, intent_category: str, parent: Optional["QObject"] = None) -> None: super().__init__(parent) diff --git a/cura/Machines/QualityGroup.py b/cura/Machines/QualityGroup.py index 97b5e28b41..2e5e8db905 100644 --- a/cura/Machines/QualityGroup.py +++ b/cura/Machines/QualityGroup.py @@ -9,28 +9,34 @@ from UM.Util import parseBool from cura.Machines.ContainerNode import ContainerNode -## A QualityGroup represents a group of quality containers that must be applied -# to each ContainerStack when it's used. -# -# A concrete example: When there are two extruders and the user selects the -# quality type "normal", this quality type must be applied to all stacks in a -# machine, although each stack can have different containers. So one global -# profile gets put on the global stack and one extruder profile gets put on -# each extruder stack. This quality group then contains the following -# profiles (for instance): -# GlobalStack ExtruderStack 1 ExtruderStack 2 -# quality container: um3_global_normal um3_aa04_pla_normal um3_aa04_abs_normal -# -# The purpose of these quality groups is to group the containers that can be -# applied to a configuration, so that when a quality level is selected, the -# container can directly be applied to each stack instead of looking them up -# again. class QualityGroup: - ## Constructs a new group. - # \param name The user-visible name for the group. - # \param quality_type The quality level that each profile in this group - # has. + """A QualityGroup represents a group of quality containers that must be applied to each ContainerStack when it's + used. + + A concrete example: When there are two extruders and the user selects the quality type "normal", this quality + type must be applied to all stacks in a machine, although each stack can have different containers. So one global + profile gets put on the global stack and one extruder profile gets put on each extruder stack. This quality group + then contains the following profiles (for instance): + - GlobalStack + - ExtruderStack 1 + - ExtruderStack 2 + quality container: + - um3_global_normal + - um3_aa04_pla_normal + - um3_aa04_abs_normal + + The purpose of these quality groups is to group the containers that can be applied to a configuration, + so that when a quality level is selected, the container can directly be applied to each stack instead of looking + them up again. + """ + def __init__(self, name: str, quality_type: str) -> None: + """Constructs a new group. + + :param name: The user-visible name for the group. + :param quality_type: The quality level that each profile in this group has. + """ + self.name = name self.node_for_global = None # type: Optional[ContainerNode] self.nodes_for_extruders = {} # type: Dict[int, ContainerNode] diff --git a/cura/Machines/QualityNode.py b/cura/Machines/QualityNode.py index 7696dfb117..dcbe486952 100644 --- a/cura/Machines/QualityNode.py +++ b/cura/Machines/QualityNode.py @@ -13,12 +13,14 @@ if TYPE_CHECKING: from cura.Machines.MachineNode import MachineNode -## Represents a quality profile in the container tree. -# -# This may either be a normal quality profile or a global quality profile. -# -# Its subcontainers are intent profiles. class QualityNode(ContainerNode): + """Represents a quality profile in the container tree. + + This may either be a normal quality profile or a global quality profile. + + Its subcontainers are intent profiles. + """ + def __init__(self, container_id: str, parent: Union["MaterialNode", "MachineNode"]) -> None: super().__init__(container_id) self.parent = parent diff --git a/cura/Machines/VariantNode.py b/cura/Machines/VariantNode.py index 0f30782a91..39664946a3 100644 --- a/cura/Machines/VariantNode.py +++ b/cura/Machines/VariantNode.py @@ -17,16 +17,16 @@ if TYPE_CHECKING: from cura.Machines.MachineNode import MachineNode -## This class represents an extruder variant in the container tree. -# -# The subnodes of these nodes are materials. -# -# This node contains materials with ALL filament diameters underneath it. The -# tree of this variant is not specific to one global stack, so because the -# list of materials can be different per stack depending on the compatible -# material diameter setting, we cannot filter them here. Filtering must be -# done in the model. class VariantNode(ContainerNode): + """This class represents an extruder variant in the container tree. + + The subnodes of these nodes are materials. + + This node contains materials with ALL filament diameters underneath it. The tree of this variant is not specific + to one global stack, so because the list of materials can be different per stack depending on the compatible + material diameter setting, we cannot filter them here. Filtering must be done in the model. + """ + def __init__(self, container_id: str, machine: "MachineNode") -> None: super().__init__(container_id) self.machine = machine @@ -39,9 +39,10 @@ class VariantNode(ContainerNode): container_registry.containerRemoved.connect(self._materialRemoved) self._loadAll() - ## (Re)loads all materials under this variant. @UM.FlameProfiler.profile def _loadAll(self) -> None: + """(Re)loads all materials under this variant.""" + container_registry = ContainerRegistry.getInstance() if not self.machine.has_materials: @@ -69,29 +70,29 @@ class VariantNode(ContainerNode): if not self.materials: self.materials["empty_material"] = MaterialNode("empty_material", variant = self) - ## Finds the preferred material for this printer with this nozzle in one of - # the extruders. - # - # If the preferred material is not available, an arbitrary material is - # returned. If there is a configuration mistake (like a typo in the - # preferred material) this returns a random available material. If there - # are no available materials, this will return the empty material node. - # \param approximate_diameter The desired approximate diameter of the - # material. - # \return The node for the preferred material, or any arbitrary material - # if there is no match. def preferredMaterial(self, approximate_diameter: int) -> MaterialNode: + """Finds the preferred material for this printer with this nozzle in one of the extruders. + + If the preferred material is not available, an arbitrary material is returned. If there is a configuration + mistake (like a typo in the preferred material) this returns a random available material. If there are no + available materials, this will return the empty material node. + + :param approximate_diameter: The desired approximate diameter of the material. + + :return: The node for the preferred material, or any arbitrary material if there is no match. + """ + for base_material, material_node in self.materials.items(): if self.machine.preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")): return material_node - + # First fallback: Check if we should be checking for the 175 variant. if approximate_diameter == 2: preferred_material = self.machine.preferred_material + "_175" for base_material, material_node in self.materials.items(): if preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")): return material_node - + # Second fallback: Choose any material with matching diameter. for material_node in self.materials.values(): if material_node.getMetaDataEntry("approximate_diameter") and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")): @@ -107,10 +108,10 @@ class VariantNode(ContainerNode): )) return fallback - ## When a material gets added to the set of profiles, we need to update our - # tree here. @UM.FlameProfiler.profile def _materialAdded(self, container: ContainerInterface) -> None: + """When a material gets added to the set of profiles, we need to update our tree here.""" + if container.getMetaDataEntry("type") != "material": return # Not interested. if not ContainerRegistry.getInstance().findContainersMetadata(id = container.getId()): diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py index cc809abf05..e825afd2a9 100644 --- a/cura/OAuth2/AuthorizationHelpers.py +++ b/cura/OAuth2/AuthorizationHelpers.py @@ -16,23 +16,27 @@ from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settin catalog = i18nCatalog("cura") TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S" -## Class containing several helpers to deal with the authorization flow. class AuthorizationHelpers: + """Class containing several helpers to deal with the authorization flow.""" + def __init__(self, settings: "OAuth2Settings") -> None: self._settings = settings self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL) @property - ## The OAuth2 settings object. def settings(self) -> "OAuth2Settings": + """The OAuth2 settings object.""" + return self._settings - ## Request the access token from the authorization server. - # \param authorization_code: The authorization code from the 1st step. - # \param verification_code: The verification code needed for the PKCE - # extension. - # \return An AuthenticationResponse object. def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse": + """Request the access token from the authorization server. + + :param authorization_code: The authorization code from the 1st step. + :param verification_code: The verification code needed for the PKCE extension. + :return: An AuthenticationResponse object. + """ + data = { "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", "redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "", @@ -46,10 +50,13 @@ class AuthorizationHelpers: except requests.exceptions.ConnectionError: return AuthenticationResponse(success=False, err_message="Unable to connect to remote server") - ## Request the access token from the authorization server using a refresh token. - # \param refresh_token: - # \return An AuthenticationResponse object. def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse": + """Request the access token from the authorization server using a refresh token. + + :param refresh_token: + :return: An AuthenticationResponse object. + """ + Logger.log("d", "Refreshing the access token.") data = { "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", @@ -64,10 +71,13 @@ class AuthorizationHelpers: return AuthenticationResponse(success=False, err_message="Unable to connect to remote server") @staticmethod - ## Parse the token response from the authorization server into an AuthenticationResponse object. - # \param token_response: The JSON string data response from the authorization server. - # \return An AuthenticationResponse object. def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse": + """Parse the token response from the authorization server into an AuthenticationResponse object. + + :param token_response: The JSON string data response from the authorization server. + :return: An AuthenticationResponse object. + """ + token_data = None try: @@ -89,10 +99,13 @@ class AuthorizationHelpers: scope=token_data["scope"], received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT)) - ## Calls the authentication API endpoint to get the token data. - # \param access_token: The encoded JWT token. - # \return Dict containing some profile data. def parseJWT(self, access_token: str) -> Optional["UserProfile"]: + """Calls the authentication API endpoint to get the token data. + + :param access_token: The encoded JWT token. + :return: Dict containing some profile data. + """ + try: token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = { "Authorization": "Bearer {}".format(access_token) @@ -115,16 +128,22 @@ class AuthorizationHelpers: ) @staticmethod - ## Generate a verification code of arbitrary length. - # \param code_length: How long should the code be? This should never be lower than 16, but it's probably better to - # leave it at 32 def generateVerificationCode(code_length: int = 32) -> str: + """Generate a verification code of arbitrary length. + + :param code_length:: How long should the code be? This should never be lower than 16, but it's probably + better to leave it at 32 + """ + return "".join(random.choice("0123456789ABCDEF") for i in range(code_length)) @staticmethod - ## Generates a base64 encoded sha512 encrypted version of a given string. - # \param verification_code: - # \return The encrypted code in base64 format. def generateVerificationCodeChallenge(verification_code: str) -> str: + """Generates a base64 encoded sha512 encrypted version of a given string. + + :param verification_code: + :return: The encrypted code in base64 format. + """ + encoded = sha512(verification_code.encode()).digest() return b64encode(encoded, altchars = b"_-").decode() diff --git a/cura/OAuth2/AuthorizationRequestHandler.py b/cura/OAuth2/AuthorizationRequestHandler.py index b002039491..c7ce9b6faf 100644 --- a/cura/OAuth2/AuthorizationRequestHandler.py +++ b/cura/OAuth2/AuthorizationRequestHandler.py @@ -14,9 +14,12 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## This handler handles all HTTP requests on the local web server. -# It also requests the access token for the 2nd stage of the OAuth flow. class AuthorizationRequestHandler(BaseHTTPRequestHandler): + """This handler handles all HTTP requests on the local web server. + + It also requests the access token for the 2nd stage of the OAuth flow. + """ + def __init__(self, request, client_address, server) -> None: super().__init__(request, client_address, server) @@ -55,10 +58,13 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler): # This will cause the server to shut down, so we do it at the very end of the request handling. self.authorization_callback(token_response) - ## Handler for the callback URL redirect. - # \param query Dict containing the HTTP query parameters. - # \return HTTP ResponseData containing a success page to show to the user. def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]: + """Handler for the callback URL redirect. + + :param query: Dict containing the HTTP query parameters. + :return: HTTP ResponseData containing a success page to show to the user. + """ + code = self._queryGet(query, "code") state = self._queryGet(query, "state") if state != self.state: @@ -95,9 +101,10 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler): self.authorization_helpers.settings.AUTH_FAILED_REDIRECT ), token_response - ## Handle all other non-existing server calls. @staticmethod def _handleNotFound() -> ResponseData: + """Handle all other non-existing server calls.""" + return ResponseData(status = HTTP_STATUS["NOT_FOUND"], content_type = "text/html", data_stream = b"Not found.") def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None: @@ -110,7 +117,8 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler): def _sendData(self, data: bytes) -> None: self.wfile.write(data) - ## Convenience helper for getting values from a pre-parsed query string @staticmethod def _queryGet(query_data: Dict[Any, List], key: str, default: Optional[str] = None) -> Optional[str]: + """Convenience helper for getting values from a pre-parsed query string""" + return query_data.get(key, [default])[0] diff --git a/cura/OAuth2/AuthorizationRequestServer.py b/cura/OAuth2/AuthorizationRequestServer.py index 687bbf5ad8..4ed3975638 100644 --- a/cura/OAuth2/AuthorizationRequestServer.py +++ b/cura/OAuth2/AuthorizationRequestServer.py @@ -2,6 +2,7 @@ # Cura is released under the terms of the LGPLv3 or higher. from http.server import HTTPServer +from socketserver import ThreadingMixIn from typing import Callable, Any, TYPE_CHECKING if TYPE_CHECKING: @@ -9,21 +10,26 @@ if TYPE_CHECKING: from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers -## The authorization request callback handler server. -# This subclass is needed to be able to pass some data to the request handler. -# This cannot be done on the request handler directly as the HTTPServer -# creates an instance of the handler after init. -class AuthorizationRequestServer(HTTPServer): - ## Set the authorization helpers instance on the request handler. +class AuthorizationRequestServer(ThreadingMixIn, HTTPServer): + """The authorization request callback handler server. + + This subclass is needed to be able to pass some data to the request handler. This cannot be done on the request + handler directly as the HTTPServer creates an instance of the handler after init. + """ + def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None: + """Set the authorization helpers instance on the request handler.""" + self.RequestHandlerClass.authorization_helpers = authorization_helpers # type: ignore - ## Set the authorization callback on the request handler. def setAuthorizationCallback(self, authorization_callback: Callable[["AuthenticationResponse"], Any]) -> None: + """Set the authorization callback on the request handler.""" + self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore - ## Set the verification code on the request handler. def setVerificationCode(self, verification_code: str) -> None: + """Set the verification code on the request handler.""" + self.RequestHandlerClass.verification_code = verification_code # type: ignore def setState(self, state: str) -> None: diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index 47e6c139b8..9a5c81ae55 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -26,9 +26,11 @@ if TYPE_CHECKING: MYCLOUD_LOGOFF_URL = "https://mycloud.ultimaker.com/logoff" -## The authorization service is responsible for handling the login flow, -# storing user credentials and providing account information. class AuthorizationService: + """The authorization service is responsible for handling the login flow, storing user credentials and providing + account information. + """ + # Emit signal when authentication is completed. onAuthStateChanged = Signal() @@ -60,11 +62,16 @@ class AuthorizationService: if self._preferences: self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}") - ## Get the user profile as obtained from the JWT (JSON Web Token). - # If the JWT is not yet parsed, calling this will take care of that. - # \return UserProfile if a user is logged in, None otherwise. - # \sa _parseJWT def getUserProfile(self) -> Optional["UserProfile"]: + """Get the user profile as obtained from the JWT (JSON Web Token). + + If the JWT is not yet parsed, calling this will take care of that. + + :return: UserProfile if a user is logged in, None otherwise. + + See also: :py:method:`cura.OAuth2.AuthorizationService.AuthorizationService._parseJWT` + """ + if not self._user_profile: # If no user profile was stored locally, we try to get it from JWT. try: @@ -82,9 +89,12 @@ class AuthorizationService: return self._user_profile - ## Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there. - # \return UserProfile if it was able to parse, None otherwise. def _parseJWT(self) -> Optional["UserProfile"]: + """Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there. + + :return: UserProfile if it was able to parse, None otherwise. + """ + if not self._auth_data or self._auth_data.access_token is None: # If no auth data exists, we should always log in again. Logger.log("d", "There was no auth data or access token") @@ -107,8 +117,9 @@ class AuthorizationService: self._storeAuthData(self._auth_data) return self._auth_helpers.parseJWT(self._auth_data.access_token) - ## Get the access token as provided by the repsonse data. def getAccessToken(self) -> Optional[str]: + """Get the access token as provided by the repsonse data.""" + if self._auth_data is None: Logger.log("d", "No auth data to retrieve the access_token from") return None @@ -123,8 +134,9 @@ class AuthorizationService: return self._auth_data.access_token if self._auth_data else None - ## Try to refresh the access token. This should be used when it has expired. def refreshAccessToken(self) -> None: + """Try to refresh the access token. This should be used when it has expired.""" + if self._auth_data is None or self._auth_data.refresh_token is None: Logger.log("w", "Unable to refresh access token, since there is no refresh token.") return @@ -136,14 +148,16 @@ class AuthorizationService: Logger.log("w", "Failed to get a new access token from the server.") self.onAuthStateChanged.emit(logged_in = False) - ## Delete the authentication data that we have stored locally (eg; logout) def deleteAuthData(self) -> None: + """Delete the authentication data that we have stored locally (eg; logout)""" + if self._auth_data is not None: self._storeAuthData() self.onAuthStateChanged.emit(logged_in = False) - ## Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login. def startAuthorizationFlow(self, force_browser_logout: bool = False) -> None: + """Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.""" + Logger.log("d", "Starting new OAuth2 flow...") # Create the tokens needed for the code challenge (PKCE) extension for OAuth2. @@ -197,8 +211,9 @@ class AuthorizationService: auth_url = "{}?next={}".format(MYCLOUD_LOGOFF_URL, quote_plus(auth_url)) return auth_url - ## Callback method for the authentication flow. def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None: + """Callback method for the authentication flow.""" + if auth_response.success: self._storeAuthData(auth_response) self.onAuthStateChanged.emit(logged_in = True) @@ -206,8 +221,9 @@ class AuthorizationService: self.onAuthenticationError.emit(logged_in = False, error_message = auth_response.err_message) self._server.stop() # Stop the web server at all times. - ## Load authentication data from preferences. def loadAuthDataFromPreferences(self) -> None: + """Load authentication data from preferences.""" + if self._preferences is None: Logger.log("e", "Unable to load authentication data, since no preference has been set!") return @@ -228,13 +244,14 @@ class AuthorizationService: except ValueError: Logger.logException("w", "Could not load auth data from preferences") - ## Store authentication data in preferences. def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None: + """Store authentication data in preferences.""" + Logger.log("d", "Attempting to store the auth data") if self._preferences is None: Logger.log("e", "Unable to save authentication data, since no preference has been set!") return - + self._auth_data = auth_data if auth_data: self._user_profile = self.getUserProfile() diff --git a/cura/OAuth2/LocalAuthorizationServer.py b/cura/OAuth2/LocalAuthorizationServer.py index 0e4e491e46..ac14b00985 100644 --- a/cura/OAuth2/LocalAuthorizationServer.py +++ b/cura/OAuth2/LocalAuthorizationServer.py @@ -1,6 +1,6 @@ # Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - +import sys import threading from typing import Any, Callable, Optional, TYPE_CHECKING @@ -20,18 +20,23 @@ if TYPE_CHECKING: class LocalAuthorizationServer: - ## The local LocalAuthorizationServer takes care of the oauth2 callbacks. - # Once the flow is completed, this server should be closed down again by - # calling stop() - # \param auth_helpers An instance of the authorization helpers class. - # \param auth_state_changed_callback A callback function to be called when - # the authorization state changes. - # \param daemon Whether the server thread should be run in daemon mode. - # Note: Daemon threads are abruptly stopped at shutdown. Their resources - # (e.g. open files) may never be released. def __init__(self, auth_helpers: "AuthorizationHelpers", auth_state_changed_callback: Callable[["AuthenticationResponse"], Any], daemon: bool) -> None: + """The local LocalAuthorizationServer takes care of the oauth2 callbacks. + + Once the flow is completed, this server should be closed down again by calling + :py:meth:`cura.OAuth2.LocalAuthorizationServer.LocalAuthorizationServer.stop()` + + :param auth_helpers: An instance of the authorization helpers class. + :param auth_state_changed_callback: A callback function to be called when the authorization state changes. + :param daemon: Whether the server thread should be run in daemon mode. + + .. note:: + + Daemon threads are abruptly stopped at shutdown. Their resources (e.g. open files) may never be released. + """ + self._web_server = None # type: Optional[AuthorizationRequestServer] self._web_server_thread = None # type: Optional[threading.Thread] self._web_server_port = auth_helpers.settings.CALLBACK_PORT @@ -39,10 +44,13 @@ class LocalAuthorizationServer: self._auth_state_changed_callback = auth_state_changed_callback self._daemon = daemon - ## Starts the local web server to handle the authorization callback. - # \param verification_code The verification code part of the OAuth2 client identification. - # \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: + """Starts the local web server to handle the authorization callback. + + :param verification_code: The verification code part of the OAuth2 client identification. + :param state: The unique state code (to ensure that the request we get back is really from the 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. # We still inject the new verification code though. @@ -63,18 +71,37 @@ class LocalAuthorizationServer: self._web_server.setState(state) # 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._serve_forever, daemon = self._daemon) self._web_server_thread.start() - ## Stops the web server if it was running. It also does some cleanup. def stop(self) -> None: + """Stops the web server if it was running. It also does some cleanup.""" + Logger.log("d", "Stopping local oauth2 web server...") if self._web_server: try: - self._web_server.server_close() + self._web_server.shutdown() except OSError: # OS error can happen if the socket was already closed. We really don't care about that case. pass self._web_server = None self._web_server_thread = None + + def _serve_forever(self) -> None: + """ + If the platform is windows, this function calls the serve_forever function of the _web_server, catching any + OSErrors that may occur in the thread, thus making the reported message more log-friendly. + If it is any other platform, it just calls the serve_forever function immediately. + + :return: None + """ + if self._web_server: + if sys.platform == "win32": + try: + self._web_server.serve_forever() + except OSError as e: + Logger.warning(str(e)) + else: + # Leave the default behavior in non-windows platforms + self._web_server.serve_forever() diff --git a/cura/OAuth2/Models.py b/cura/OAuth2/Models.py index dd935fef6e..93b44e8057 100644 --- a/cura/OAuth2/Models.py +++ b/cura/OAuth2/Models.py @@ -8,8 +8,9 @@ class BaseModel: self.__dict__.update(kwargs) -## OAuth OAuth2Settings data template. class OAuth2Settings(BaseModel): + """OAuth OAuth2Settings data template.""" + CALLBACK_PORT = None # type: Optional[int] OAUTH_SERVER_URL = None # type: Optional[str] CLIENT_ID = None # type: Optional[str] @@ -20,16 +21,18 @@ class OAuth2Settings(BaseModel): AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str -## User profile data template. class UserProfile(BaseModel): + """User profile data template.""" + user_id = None # type: Optional[str] username = None # type: Optional[str] profile_image_url = None # type: Optional[str] -## Authentication data template. class AuthenticationResponse(BaseModel): - """Data comes from the token response with success flag and error message added.""" + """Authentication data template.""" + + # Data comes from the token response with success flag and error message added. success = True # type: bool token_type = None # type: Optional[str] access_token = None # type: Optional[str] @@ -40,22 +43,25 @@ class AuthenticationResponse(BaseModel): received_at = None # type: Optional[str] -## Response status template. class ResponseStatus(BaseModel): + """Response status template.""" + code = 200 # type: int message = "" # type: str -## Response data template. class ResponseData(BaseModel): + """Response data template.""" + status = None # type: ResponseStatus data_stream = None # type: Optional[bytes] redirect_uri = None # type: Optional[str] content_type = "text/html" # type: str -## Possible HTTP responses. HTTP_STATUS = { +"""Possible HTTP responses.""" + "OK": ResponseStatus(code = 200, message = "OK"), "NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"), "REDIRECT": ResponseStatus(code = 302, message = "REDIRECT") diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py index 3373f2104f..def0dac4fe 100644 --- a/cura/OneAtATimeIterator.py +++ b/cura/OneAtATimeIterator.py @@ -7,18 +7,21 @@ from UM.Scene.Iterator import Iterator from UM.Scene.SceneNode import SceneNode from functools import cmp_to_key -## Iterator that returns a list of nodes in the order that they need to be printed -# If there is no solution an empty list is returned. -# Take note that the list of nodes can have children (that may or may not contain mesh data) class OneAtATimeIterator(Iterator.Iterator): + """Iterator that returns a list of nodes in the order that they need to be printed + + If there is no solution an empty list is returned. + Take note that the list of nodes can have children (that may or may not contain mesh data) + """ + def __init__(self, scene_node) -> None: super().__init__(scene_node) # Call super to make multiple inheritance work. self._hit_map = [[]] # type: List[List[bool]] # For each node, which other nodes this hits. A grid of booleans on which nodes hit which. self._original_node_list = [] # type: List[SceneNode] # The nodes that need to be checked for collisions. - ## Fills the ``_node_stack`` with a list of scene nodes that need to be - # printed in order. def _fillStack(self) -> None: + """Fills the ``_node_stack`` with a list of scene nodes that need to be printed in order. """ + node_list = [] for node in self._scene_node.getChildren(): if not issubclass(type(node), SceneNode): @@ -75,10 +78,14 @@ class OneAtATimeIterator(Iterator.Iterator): return True return False - ## Check for a node whether it hits any of the other nodes. - # \param node The node to check whether it collides with the other nodes. - # \param other_nodes The nodes to check for collisions. def _checkBlockMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool: + """Check for a node whether it hits any of the other nodes. + + :param node: The node to check whether it collides with the other nodes. + :param other_nodes: The nodes to check for collisions. + :return: returns collision between nodes + """ + node_index = self._original_node_list.index(node) for other_node in other_nodes: other_node_index = self._original_node_list.index(other_node) @@ -86,14 +93,26 @@ class OneAtATimeIterator(Iterator.Iterator): return True return False - ## Calculate score simply sums the number of other objects it 'blocks' def _calculateScore(self, a: SceneNode, b: SceneNode) -> int: + """Calculate score simply sums the number of other objects it 'blocks' + + :param a: node + :param b: node + :return: sum of the number of other objects + """ + score_a = sum(self._hit_map[self._original_node_list.index(a)]) score_b = sum(self._hit_map[self._original_node_list.index(b)]) return score_a - score_b - ## Checks if A can be printed before B def _checkHit(self, a: SceneNode, b: SceneNode) -> bool: + """Checks if a can be printed before b + + :param a: node + :param b: node + :return: true if a can be printed before b + """ + if a == b: return False @@ -116,12 +135,14 @@ class OneAtATimeIterator(Iterator.Iterator): return False -## Internal object used to keep track of a possible order in which to print objects. class _ObjectOrder: - ## Creates the _ObjectOrder instance. - # \param order List of indices in which to print objects, ordered by printing - # order. - # \param todo: List of indices which are not yet inserted into the order list. + """Internal object used to keep track of a possible order in which to print objects.""" + def __init__(self, order: List[SceneNode], todo: List[SceneNode]) -> None: + """Creates the _ObjectOrder instance. + + :param order: List of indices in which to print objects, ordered by printing order. + :param todo: List of indices which are not yet inserted into the order list. + """ self.order = order self.todo = todo diff --git a/cura/Operations/PlatformPhysicsOperation.py b/cura/Operations/PlatformPhysicsOperation.py index 0d69320eec..e433b67a7b 100644 --- a/cura/Operations/PlatformPhysicsOperation.py +++ b/cura/Operations/PlatformPhysicsOperation.py @@ -6,8 +6,9 @@ from UM.Operations.GroupedOperation import GroupedOperation from UM.Scene.SceneNode import SceneNode -## A specialised operation designed specifically to modify the previous operation. class PlatformPhysicsOperation(Operation): + """A specialised operation designed specifically to modify the previous operation.""" + def __init__(self, node: SceneNode, translation: Vector) -> None: super().__init__() self._node = node diff --git a/cura/Operations/SetBuildPlateNumberOperation.py b/cura/Operations/SetBuildPlateNumberOperation.py index fd48cf47d9..8a5bdeb442 100644 --- a/cura/Operations/SetBuildPlateNumberOperation.py +++ b/cura/Operations/SetBuildPlateNumberOperation.py @@ -7,8 +7,9 @@ from UM.Operations.Operation import Operation from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -## Simple operation to set the buildplate number of a scenenode. class SetBuildPlateNumberOperation(Operation): + """Simple operation to set the buildplate number of a scenenode.""" + def __init__(self, node: SceneNode, build_plate_nr: int) -> None: super().__init__() self._node = node diff --git a/cura/Operations/SetParentOperation.py b/cura/Operations/SetParentOperation.py index 6d603c1d82..a8fab49395 100644 --- a/cura/Operations/SetParentOperation.py +++ b/cura/Operations/SetParentOperation.py @@ -6,31 +6,37 @@ from UM.Scene.SceneNode import SceneNode from UM.Operations import Operation - -## An operation that parents a scene node to another scene node. class SetParentOperation(Operation.Operation): - ## Initialises this SetParentOperation. - # - # \param node The node which will be reparented. - # \param parent_node The node which will be the parent. + """An operation that parents a scene node to another scene node.""" + def __init__(self, node: SceneNode, parent_node: Optional[SceneNode]) -> None: + """Initialises this SetParentOperation. + + :param node: The node which will be reparented. + :param parent_node: The node which will be the parent. + """ + super().__init__() self._node = node self._parent = parent_node self._old_parent = node.getParent() # To restore the previous parent in case of an undo. - ## Undoes the set-parent operation, restoring the old parent. def undo(self) -> None: + """Undoes the set-parent operation, restoring the old parent.""" + self._set_parent(self._old_parent) - ## Re-applies the set-parent operation. def redo(self) -> None: + """Re-applies the set-parent operation.""" + self._set_parent(self._parent) - ## 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. def _set_parent(self, new_parent: Optional[SceneNode]) -> None: + """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. + """ + if new_parent: current_parent = self._node.getParent() if current_parent: @@ -56,8 +62,10 @@ class SetParentOperation(Operation.Operation): self._node.setParent(new_parent) - ## Returns a programmer-readable representation of this operation. - # - # \return A programmer-readable representation of this operation. def __repr__(self) -> str: + """Returns a programmer-readable representation of this operation. + + :return: A programmer-readable representation of this operation. + """ + return "SetParentOperation(node = {0}, parent_node={1})".format(self._node, self._parent) diff --git a/cura/PickingPass.py b/cura/PickingPass.py index ea0d05ab4f..13a115f64b 100644 --- a/cura/PickingPass.py +++ b/cura/PickingPass.py @@ -18,11 +18,15 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator if TYPE_CHECKING: from UM.View.GL.ShaderProgram import ShaderProgram -## A RenderPass subclass that renders a the distance of selectable objects from the active camera to a texture. -# The texture is used to map a 2d location (eg the mouse location) to a world space position -# -# Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels class PickingPass(RenderPass): + """A :py:class:`Uranium.UM.View.RenderPass` subclass that renders a the distance of selectable objects from the + active camera to a texture. + + The texture is used to map a 2d location (eg the mouse location) to a world space position + + .. note:: that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels + """ + def __init__(self, width: int, height: int) -> None: super().__init__("picking", width, height) @@ -56,8 +60,14 @@ class PickingPass(RenderPass): batch.render(self._scene.getActiveCamera()) self.release() - ## Get the distance in mm from the camera to at a certain pixel coordinate. def getPickedDepth(self, x: int, y: int) -> float: + """Get the distance in mm from the camera to at a certain pixel coordinate. + + :param x: x component of coordinate vector in pixels + :param y: y component of coordinate vector in pixels + :return: distance in mm from the camera to pixel coordinate + """ + output = self.getOutput() window_size = self._renderer.getWindowSize() @@ -72,8 +82,14 @@ class PickingPass(RenderPass): distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm return distance - ## Get the world coordinates of a picked point def getPickedPosition(self, x: int, y: int) -> Vector: + """Get the world coordinates of a picked point + + :param x: x component of coordinate vector in pixels + :param y: y component of coordinate vector in pixels + :return: vector of the world coordinate + """ + distance = self.getPickedDepth(x, y) camera = self._scene.getActiveCamera() if camera: diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 015795e506..5fd2e70a1c 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -95,15 +95,15 @@ class PlatformPhysics: # Ignore root, ourselves and anything that is not a normal SceneNode. if other_node is root or not issubclass(type(other_node), SceneNode) or other_node is node or other_node.callDecoration("getBuildPlateNumber") != node.callDecoration("getBuildPlateNumber"): continue - + # Ignore collisions of a group with it's own children if other_node in node.getAllChildren() or node in other_node.getAllChildren(): continue - + # Ignore collisions within a group if other_node.getParent() and node.getParent() and (other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None): continue - + # Ignore nodes that do not have the right properties set. if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox(): continue diff --git a/cura/PreviewPass.py b/cura/PreviewPass.py index 7fcc4eb6cd..9cae5ad9bd 100644 --- a/cura/PreviewPass.py +++ b/cura/PreviewPass.py @@ -1,7 +1,7 @@ # Copyright (c) 2020 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, TYPE_CHECKING, cast +from typing import Optional, TYPE_CHECKING, cast, List from UM.Application import Application @@ -21,9 +21,14 @@ if TYPE_CHECKING: from UM.Scene.Camera import Camera -# Make color brighter by normalizing it (maximum factor 2.5 brighter) -# color_list is a list of 4 elements: [r, g, b, a], each element is a float 0..1 -def prettier_color(color_list): +def prettier_color(color_list: List[float]) -> List[float]: + """Make color brighter by normalizing + + maximum factor 2.5 brighter + + :param color_list: a list of 4 elements: [r, g, b, a], each element is a float 0..1 + :return: a normalized list of 4 elements: [r, g, b, a], each element is a float 0..1 + """ maximum = max(color_list[:3]) if maximum > 0: factor = min(1 / maximum, 2.5) @@ -32,11 +37,14 @@ def prettier_color(color_list): return [min(i * factor, 1.0) for i in color_list] -## A render pass subclass that renders slicable objects with default parameters. -# It uses the active camera by default, but it can be overridden to use a different camera. -# -# This is useful to get a preview image of a scene taken from a different location as the active camera. class PreviewPass(RenderPass): + """A :py:class:`Uranium.UM.View.RenderPass` subclass that renders slicable objects with default parameters. + + It uses the active camera by default, but it can be overridden to use a different camera. + + This is useful to get a preview image of a scene taken from a different location as the active camera. + """ + def __init__(self, width: int, height: int) -> None: super().__init__("preview", width, height, 0) diff --git a/cura/PrintJobPreviewImageProvider.py b/cura/PrintJobPreviewImageProvider.py index 8b46c6db37..321164adeb 100644 --- a/cura/PrintJobPreviewImageProvider.py +++ b/cura/PrintJobPreviewImageProvider.py @@ -10,8 +10,14 @@ class PrintJobPreviewImageProvider(QQuickImageProvider): def __init__(self): super().__init__(QQuickImageProvider.Image) - ## Request a new image. def requestImage(self, id: str, size: QSize) -> Tuple[QImage, QSize]: + """Request a new image. + + :param id: id of the requested image + :param size: is not used defaults to QSize(15, 15) + :return: an tuple containing the image and size + """ + # 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. uuid = id[id.find("/") + 1:] diff --git a/cura/PrinterOutput/FirmwareUpdater.py b/cura/PrinterOutput/FirmwareUpdater.py index 80269b97a3..c4f3948c20 100644 --- a/cura/PrinterOutput/FirmwareUpdater.py +++ b/cura/PrinterOutput/FirmwareUpdater.py @@ -7,6 +7,8 @@ from enum import IntEnum from threading import Thread from typing import Union +from UM.Logger import Logger + MYPY = False if MYPY: from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice @@ -36,16 +38,19 @@ class FirmwareUpdater(QObject): if self._firmware_file == "": self._setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error) return - - self._setFirmwareUpdateState(FirmwareUpdateState.updating) - self._update_firmware_thread.start() + self._setFirmwareUpdateState(FirmwareUpdateState.updating) + try: + self._update_firmware_thread.start() + except RuntimeError: + Logger.warning("Could not start the update thread, since it's still running!") def _updateFirmware(self) -> None: raise NotImplementedError("_updateFirmware needs to be implemented") - ## Cleanup after a succesful update def _cleanupAfterUpdate(self) -> None: + """Cleanup after a succesful update""" + # Clean up for next attempt. self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True, name = "FirmwareUpdateThread") self._firmware_file = "" diff --git a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py index 4a1cf4916f..ecc855ab8f 100644 --- a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py +++ b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py @@ -47,10 +47,13 @@ class ExtruderConfigurationModel(QObject): def hotendID(self) -> Optional[str]: return self._hotend_id - ## This method is intended to indicate whether the configuration is valid or not. - # The method checks if the mandatory fields are or not set - # At this moment is always valid since we allow to have empty material and variants. def isValid(self) -> bool: + """This method is intended to indicate whether the configuration is valid or not. + + The method checks if the mandatory fields are or not set + At this moment is always valid since we allow to have empty material and variants. + """ + return True def __str__(self) -> str: diff --git a/cura/PrinterOutput/Models/ExtruderOutputModel.py b/cura/PrinterOutput/Models/ExtruderOutputModel.py index 889e140312..9da3a7117d 100644 --- a/cura/PrinterOutput/Models/ExtruderOutputModel.py +++ b/cura/PrinterOutput/Models/ExtruderOutputModel.py @@ -54,8 +54,9 @@ class ExtruderOutputModel(QObject): def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]) -> None: self._extruder_configuration.setMaterial(material) - ## Update the hotend temperature. This only changes it locally. def updateHotendTemperature(self, temperature: float) -> None: + """Update the hotend temperature. This only changes it locally.""" + if self._hotend_temperature != temperature: self._hotend_temperature = temperature self.hotendTemperatureChanged.emit() @@ -65,9 +66,10 @@ class ExtruderOutputModel(QObject): self._target_hotend_temperature = temperature self.targetHotendTemperatureChanged.emit() - ## Set the target hotend temperature. This ensures that it's actually sent to the remote. @pyqtSlot(float) def setTargetHotendTemperature(self, temperature: float) -> None: + """Set the target hotend temperature. This ensures that it's actually sent to the remote.""" + self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature) self.updateTargetHotendTemperature(temperature) @@ -101,13 +103,15 @@ class ExtruderOutputModel(QObject): def isPreheating(self) -> bool: return self._is_preheating - ## Pre-heats the extruder before printer. - # - # \param temperature The temperature to heat the extruder to, in degrees - # Celsius. - # \param duration How long the bed should stay warm, in seconds. @pyqtSlot(float, float) def preheatHotend(self, temperature: float, duration: float) -> None: + """Pre-heats the extruder before printer. + + :param temperature: The temperature to heat the extruder to, in degrees + Celsius. + :param duration: How long the bed should stay warm, in seconds. + """ + self._printer._controller.preheatHotend(self, temperature, duration) @pyqtSlot() diff --git a/cura/PrinterOutput/Models/PrinterConfigurationModel.py b/cura/PrinterOutput/Models/PrinterConfigurationModel.py index 52c7b6f960..54f52134b2 100644 --- a/cura/PrinterOutput/Models/PrinterConfigurationModel.py +++ b/cura/PrinterOutput/Models/PrinterConfigurationModel.py @@ -48,9 +48,11 @@ class PrinterConfigurationModel(QObject): def buildplateConfiguration(self) -> str: return self._buildplate_configuration - ## This method is intended to indicate whether the configuration is valid or not. - # The method checks if the mandatory fields are or not set def isValid(self) -> bool: + """This method is intended to indicate whether the configuration is valid or not. + + The method checks if the mandatory fields are or not set + """ if not self._extruder_configurations: return False for configuration in self._extruder_configurations: @@ -97,9 +99,11 @@ class PrinterConfigurationModel(QObject): return True - ## The hash function is used to compare and create unique sets. The configuration is unique if the configuration - # of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same. def __hash__(self): + """The hash function is used to compare and create unique sets. The configuration is unique if the configuration + + of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same. + """ extruder_hash = hash(0) first_extruder = None for configuration in self._extruder_configurations: diff --git a/cura/PrinterOutput/Models/PrinterOutputModel.py b/cura/PrinterOutput/Models/PrinterOutputModel.py index 37135bf663..37464b0b7d 100644 --- a/cura/PrinterOutput/Models/PrinterOutputModel.py +++ b/cura/PrinterOutput/Models/PrinterOutputModel.py @@ -163,13 +163,15 @@ class PrinterOutputModel(QObject): def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None: self._controller.moveHead(self, x, y, z, speed) - ## Pre-heats the heated bed of the printer. - # - # \param temperature The temperature to heat the bed to, in degrees - # Celsius. - # \param duration How long the bed should stay warm, in seconds. @pyqtSlot(float, float) def preheatBed(self, temperature: float, duration: float) -> None: + """Pre-heats the heated bed of the printer. + + :param temperature: The temperature to heat the bed to, in degrees + Celsius. + :param duration: How long the bed should stay warm, in seconds. + """ + self._controller.preheatBed(self, temperature, duration) @pyqtSlot() @@ -200,8 +202,9 @@ class PrinterOutputModel(QObject): self._unique_name = unique_name self.nameChanged.emit() - ## Update the bed temperature. This only changes it locally. def updateBedTemperature(self, temperature: float) -> None: + """Update the bed temperature. This only changes it locally.""" + if self._bed_temperature != temperature: self._bed_temperature = temperature self.bedTemperatureChanged.emit() @@ -211,9 +214,10 @@ class PrinterOutputModel(QObject): self._target_bed_temperature = temperature self.targetBedTemperatureChanged.emit() - ## Set the target bed temperature. This ensures that it's actually sent to the remote. @pyqtSlot(float) def setTargetBedTemperature(self, temperature: float) -> None: + """Set the target bed temperature. This ensures that it's actually sent to the remote.""" + self._controller.setTargetBedTemperature(self, temperature) self.updateTargetBedTemperature(temperature) diff --git a/cura/PrinterOutput/NetworkMJPGImage.py b/cura/PrinterOutput/NetworkMJPGImage.py index 42132a7880..0bfcfab764 100644 --- a/cura/PrinterOutput/NetworkMJPGImage.py +++ b/cura/PrinterOutput/NetworkMJPGImage.py @@ -32,8 +32,9 @@ class NetworkMJPGImage(QQuickPaintedItem): self.setAntialiasing(True) - ## Ensure that close gets called when object is destroyed def __del__(self) -> None: + """Ensure that close gets called when object is destroyed""" + self.stop() diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 60be5bc8f3..2690c2651f 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -84,8 +84,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def _compressGCode(self) -> Optional[bytes]: self._compressing_gcode = True - ## Mash the data into single string max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line. + """Mash the data into single string""" file_data_bytes_list = [] batched_lines = [] batched_lines_count = 0 @@ -145,9 +145,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) return request - ## This method was only available privately before, but it was actually called from SendMaterialJob.py. - # We now have a public equivalent as well. We did not remove the private one as plugins might be using that. def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: + """This method was only available privately before, but it was actually called from SendMaterialJob.py. + + We now have a public equivalent as well. We did not remove the private one as plugins might be using that. + """ return self._createFormPart(content_header, data, content_type) def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: @@ -163,8 +165,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): part.setBody(data) return part - ## Convenience function to get the username, either from the cloud or from the OS. def _getUserName(self) -> str: + """Convenience function to get the username, either from the cloud or from the OS.""" + # check first if we are logged in with the Ultimaker Account account = CuraApplication.getInstance().getCuraAPI().account # type: Account if account and account.isLoggedIn: @@ -187,15 +190,17 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._createNetworkManager() assert (self._manager is not None) - ## Sends a put request to the given path. - # \param url: The path after the API prefix. - # \param data: The data to be sent in the body - # \param content_type: The content type of the body data. - # \param on_finished: The function to call when the response is received. - # \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total. def put(self, url: str, data: Union[str, bytes], content_type: Optional[str] = "application/json", on_finished: Optional[Callable[[QNetworkReply], None]] = None, on_progress: Optional[Callable[[int, int], None]] = None) -> None: + """Sends a put request to the given path. + + :param url: The path after the API prefix. + :param data: The data to be sent in the body + :param content_type: The content type of the body data. + :param on_finished: The function to call when the response is received. + :param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total. + """ self._validateManager() request = self._createEmptyRequest(url, content_type = content_type) @@ -212,10 +217,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if on_progress is not None: reply.uploadProgress.connect(on_progress) - ## Sends a delete request to the given path. - # \param url: The path after the API prefix. - # \param on_finished: The function to be call when the response is received. def delete(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: + """Sends a delete request to the given path. + + :param url: The path after the API prefix. + :param on_finished: The function to be call when the response is received. + """ self._validateManager() request = self._createEmptyRequest(url) @@ -228,10 +235,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): reply = self._manager.deleteResource(request) self._registerOnFinishedCallback(reply, on_finished) - ## Sends a get request to the given path. - # \param url: The path after the API prefix. - # \param on_finished: The function to be call when the response is received. def get(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: + """Sends a get request to the given path. + + :param url: The path after the API prefix. + :param on_finished: The function to be call when the response is received. + """ self._validateManager() request = self._createEmptyRequest(url) @@ -244,14 +253,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): reply = self._manager.get(request) self._registerOnFinishedCallback(reply, on_finished) - ## Sends a post request to the given path. - # \param url: The path after the API prefix. - # \param data: The data to be sent in the body - # \param on_finished: The function to call when the response is received. - # \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total. def post(self, url: str, data: Union[str, bytes], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Optional[Callable[[int, int], None]] = None) -> None: + + """Sends a post request to the given path. + + :param url: The path after the API prefix. + :param data: The data to be sent in the body + :param on_finished: The function to call when the response is received. + :param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total. + """ + self._validateManager() request = self._createEmptyRequest(url) @@ -318,10 +331,13 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if on_finished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished - ## This method checks if the name of the group stored in the definition container is correct. - # After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group - # then all the container stacks are updated, both the current and the hidden ones. def _checkCorrectGroupName(self, device_id: str, group_name: str) -> None: + """This method checks if the name of the group stored in the definition container is correct. + + After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group + then all the container stacks are updated, both the current and the hidden ones. + """ + global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() active_machine_network_name = CuraApplication.getInstance().getMachineManager().activeMachineNetworkKey() if global_container_stack and device_id == active_machine_network_name: @@ -366,32 +382,38 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def getProperties(self): return self._properties - ## Get the unique key of this machine - # \return key String containing the key of the machine. @pyqtProperty(str, constant = True) def key(self) -> str: + """Get the unique key of this machine + + :return: key String containing the key of the machine. + """ return self._id - ## The IP address of the printer. @pyqtProperty(str, constant = True) def address(self) -> str: + """The IP address of the printer.""" + return self._properties.get(b"address", b"").decode("utf-8") - ## Name of the printer (as returned from the ZeroConf properties) @pyqtProperty(str, constant = True) def name(self) -> str: + """Name of the printer (as returned from the ZeroConf properties)""" + return self._properties.get(b"name", b"").decode("utf-8") - ## Firmware version (as returned from the ZeroConf properties) @pyqtProperty(str, constant = True) def firmwareVersion(self) -> str: + """Firmware version (as returned from the ZeroConf properties)""" + return self._properties.get(b"firmware_version", b"").decode("utf-8") @pyqtProperty(str, constant = True) def printerType(self) -> str: return self._properties.get(b"printer_type", b"Unknown").decode("utf-8") - ## IP adress of this printer @pyqtProperty(str, constant = True) def ipAddress(self) -> str: + """IP adress of this printer""" + return self._address diff --git a/cura/PrinterOutput/Peripheral.py b/cura/PrinterOutput/Peripheral.py index 2693b82c36..27d127832b 100644 --- a/cura/PrinterOutput/Peripheral.py +++ b/cura/PrinterOutput/Peripheral.py @@ -2,15 +2,19 @@ # Cura is released under the terms of the LGPLv3 or higher. -## Data class that represents a peripheral for a printer. -# -# Output device plug-ins may specify that the printer has a certain set of -# peripherals. This set is then possibly shown in the interface of the monitor -# stage. class Peripheral: - ## Constructs the peripheral. - # \param type A unique ID for the type of peripheral. - # \param name A human-readable name for the peripheral. + """Data class that represents a peripheral for a printer. + + Output device plug-ins may specify that the printer has a certain set of + peripherals. This set is then possibly shown in the interface of the monitor + stage. + """ + def __init__(self, peripheral_type: str, name: str) -> None: + """Constructs the peripheral. + + :param peripheral_type: A unique ID for the type of peripheral. + :param name: A human-readable name for the peripheral. + """ self.type = peripheral_type self.name = name diff --git a/cura/PrinterOutput/PrinterOutputDevice.py b/cura/PrinterOutput/PrinterOutputDevice.py index 0e0ad488b1..526d713748 100644 --- a/cura/PrinterOutput/PrinterOutputDevice.py +++ b/cura/PrinterOutput/PrinterOutputDevice.py @@ -24,8 +24,9 @@ if MYPY: i18n_catalog = i18nCatalog("cura") -## The current processing state of the backend. class ConnectionState(IntEnum): + """The current processing state of the backend.""" + Closed = 0 Connecting = 1 Connected = 2 @@ -40,17 +41,19 @@ class ConnectionType(IntEnum): CloudConnection = 3 -## Printer output device adds extra interface options on top of output device. -# -# The assumption is made the printer is a FDM printer. -# -# Note that a number of settings are marked as "final". This is because decorators -# are not inherited by children. To fix this we use the private counter part of those -# functions to actually have the implementation. -# -# For all other uses it should be used in the same way as a "regular" OutputDevice. @signalemitter class PrinterOutputDevice(QObject, OutputDevice): + """Printer output device adds extra interface options on top of output device. + + The assumption is made the printer is a FDM printer. + + Note that a number of settings are marked as "final". This is because decorators + are not inherited by children. To fix this we use the private counter part of those + functions to actually have the implementation. + + For all other uses it should be used in the same way as a "regular" OutputDevice. + """ + printersChanged = pyqtSignal() connectionStateChanged = pyqtSignal(str) @@ -184,26 +187,30 @@ class PrinterOutputDevice(QObject, OutputDevice): if self._monitor_item is None: self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self}) - ## Attempt to establish connection def connect(self) -> None: + """Attempt to establish connection""" + self.setConnectionState(ConnectionState.Connecting) self._update_timer.start() - ## Attempt to close the connection def close(self) -> None: + """Attempt to close the connection""" + self._update_timer.stop() self.setConnectionState(ConnectionState.Closed) - ## Ensure that close gets called when object is destroyed def __del__(self) -> None: + """Ensure that close gets called when object is destroyed""" + self.close() @pyqtProperty(bool, notify = acceptsCommandsChanged) def acceptsCommands(self) -> bool: return self._accepts_commands - ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands def _setAcceptsCommands(self, accepts_commands: bool) -> None: + """Set a flag to signal the UI that the printer is not (yet) ready to receive commands""" + if self._accepts_commands != accepts_commands: self._accepts_commands = accepts_commands @@ -241,16 +248,20 @@ class PrinterOutputDevice(QObject, OutputDevice): # At this point there may be non-updated configurations self._updateUniqueConfigurations() - ## Set the device firmware name - # - # \param name The name of the firmware. def _setFirmwareName(self, name: str) -> None: + """Set the device firmware name + + :param name: The name of the firmware. + """ + self._firmware_name = name - ## Get the name of device firmware - # - # This name can be used to define device type def getFirmwareName(self) -> Optional[str]: + """Get the name of device firmware + + This name can be used to define device type + """ + return self._firmware_name def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]: diff --git a/cura/ReaderWriters/ProfileReader.py b/cura/ReaderWriters/ProfileReader.py index 460fce823e..0d53bdebac 100644 --- a/cura/ReaderWriters/ProfileReader.py +++ b/cura/ReaderWriters/ProfileReader.py @@ -10,15 +10,19 @@ class NoProfileException(Exception): pass -## A type of plug-ins that reads profiles from a file. -# -# The profile is then stored as instance container of the type user profile. class ProfileReader(PluginObject): + """A type of plug-ins that reads profiles from a file. + + The profile is then stored as instance container of the type user profile. + """ + def __init__(self): super().__init__() - ## Read profile data from a file and return a filled profile. - # - # \return \type{Profile|Profile[]} The profile that was obtained from the file or a list of Profiles. def read(self, file_name): + """Read profile data from a file and return a filled profile. + + :return: :type{Profile|Profile[]} The profile that was obtained from the file or a list of Profiles. + """ + raise NotImplementedError("Profile reader plug-in was not correctly implemented. The read function was not implemented.") diff --git a/cura/ReaderWriters/ProfileWriter.py b/cura/ReaderWriters/ProfileWriter.py index 5f81dc28c3..987924ccbf 100644 --- a/cura/ReaderWriters/ProfileWriter.py +++ b/cura/ReaderWriters/ProfileWriter.py @@ -3,23 +3,29 @@ from UM.PluginObject import PluginObject -## Base class for profile writer plugins. -# -# This class defines a write() function to write profiles to files with. + class ProfileWriter(PluginObject): - ## Initialises the profile writer. - # - # This currently doesn't do anything since the writer is basically static. + """Base class for profile writer plugins. + + This class defines a write() function to write profiles to files with. + """ + def __init__(self): + """Initialises the profile writer. + + This currently doesn't do anything since the writer is basically static. + """ + super().__init__() - ## Writes a profile to the specified file path. - # - # The profile writer may write its own file format to the specified file. - # - # \param path \type{string} The file to output to. - # \param profiles \type{Profile} or \type{List} The profile(s) to write to the file. - # \return \code True \endcode if the writing was successful, or \code - # False \endcode if it wasn't. def write(self, path, profiles): + """Writes a profile to the specified file path. + + The profile writer may write its own file format to the specified file. + + :param path: :type{string} The file to output to. + :param profiles: :type{Profile} or :type{List} The profile(s) to write to the file. + :return: True if the writing was successful, or False if it wasn't. + """ + raise NotImplementedError("Profile writer plugin was not correctly implemented. No write was specified.") diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index cff9f88f62..9dd9d3dc24 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -2,8 +2,9 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from cura.Scene.CuraSceneNode import CuraSceneNode -## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator. class BuildPlateDecorator(SceneNodeDecorator): + """Make a SceneNode build plate aware CuraSceneNode objects all have this decorator.""" + def __init__(self, build_plate_number: int = -1) -> None: super().__init__() self._build_plate_number = build_plate_number diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index b5f5fb4540..46caa5b9e0 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -23,9 +23,12 @@ if TYPE_CHECKING: from UM.Math.Matrix import Matrix -## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. -# If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed. class ConvexHullDecorator(SceneNodeDecorator): + """The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. + + If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed. + """ + def __init__(self) -> None: super().__init__() @@ -74,13 +77,16 @@ class ConvexHullDecorator(SceneNodeDecorator): self._onChanged() - ## Force that a new (empty) object is created upon copy. def __deepcopy__(self, memo): + """Force that a new (empty) object is created upon copy.""" + return ConvexHullDecorator() - ## The polygon representing the 2D adhesion area. - # If no adhesion is used, the regular convex hull is returned def getAdhesionArea(self) -> Optional[Polygon]: + """The polygon representing the 2D adhesion area. + + If no adhesion is used, the regular convex hull is returned + """ if self._node is None: return None @@ -90,9 +96,11 @@ class ConvexHullDecorator(SceneNodeDecorator): return self._add2DAdhesionMargin(hull) - ## Get the unmodified 2D projected convex hull of the node (if any) - # In case of one-at-a-time, this includes adhesion and head+fans clearance def getConvexHull(self) -> Optional[Polygon]: + """Get the unmodified 2D projected convex hull of the node (if any) + + In case of one-at-a-time, this includes adhesion and head+fans clearance + """ if self._node is None: return None if self._node.callDecoration("isNonPrintingMesh"): @@ -108,9 +116,11 @@ class ConvexHullDecorator(SceneNodeDecorator): return self._compute2DConvexHull() - ## For one at the time this is the convex hull of the node with the full head size - # In case of printing all at once this is None. def getConvexHullHeadFull(self) -> Optional[Polygon]: + """For one at the time this is the convex hull of the node with the full head size + + In case of printing all at once this is None. + """ if self._node is None: return None @@ -126,10 +136,12 @@ class ConvexHullDecorator(SceneNodeDecorator): return False return bool(parent.callDecoration("isGroup")) - ## Get convex hull of the object + head size - # In case of printing all at once this is None. - # For one at the time this is area with intersection of mirrored head def getConvexHullHead(self) -> Optional[Polygon]: + """Get convex hull of the object + head size + + In case of printing all at once this is None. + For one at the time this is area with intersection of mirrored head + """ if self._node is None: return None if self._node.callDecoration("isNonPrintingMesh"): @@ -142,10 +154,12 @@ class ConvexHullDecorator(SceneNodeDecorator): return head_with_fans_with_adhesion_margin return None - ## Get convex hull of the node - # In case of printing all at once this None?? - # For one at the time this is the area without the head. def getConvexHullBoundary(self) -> Optional[Polygon]: + """Get convex hull of the node + + In case of printing all at once this None?? + For one at the time this is the area without the head. + """ if self._node is None: return None @@ -157,10 +171,12 @@ class ConvexHullDecorator(SceneNodeDecorator): return self._compute2DConvexHull() return None - ## Get the buildplate polygon where will be printed - # In case of printing all at once this is the same as convex hull (no individual adhesion) - # For one at the time this includes the adhesion area def getPrintingArea(self) -> Optional[Polygon]: + """Get the buildplate polygon where will be printed + + In case of printing all at once this is the same as convex hull (no individual adhesion) + For one at the time this includes the adhesion area + """ if self._isSingularOneAtATimeNode(): # In one-at-a-time mode, every printed object gets it's own adhesion printing_area = self.getAdhesionArea() @@ -168,8 +184,9 @@ class ConvexHullDecorator(SceneNodeDecorator): printing_area = self.getConvexHull() return printing_area - ## The same as recomputeConvexHull, but using a timer if it was set. def recomputeConvexHullDelayed(self) -> None: + """The same as recomputeConvexHull, but using a timer if it was set.""" + if self._recompute_convex_hull_timer is not None: self._recompute_convex_hull_timer.start() else: @@ -224,7 +241,7 @@ class ConvexHullDecorator(SceneNodeDecorator): if self._node is None: return None if self._node.callDecoration("isGroup"): - points = numpy.zeros((0, 2), dtype=numpy.int32) + points = numpy.zeros((0, 2), dtype = numpy.int32) for child in self._node.getChildren(): child_hull = child.callDecoration("_compute2DConvexHull") if child_hull: @@ -268,7 +285,7 @@ class ConvexHullDecorator(SceneNodeDecorator): # Do not throw away vertices: the convex hull may be too small and objects can collide. # vertex_data = vertex_data[vertex_data[:,1] >= -0.01] - if len(vertex_data) >= 4: # type: ignore # mypy and numpy don't play along well just yet. + if vertex_data is not None and len(vertex_data) >= 4: # type: ignore # mypy and numpy don't play along well just yet. # Round the vertex data to 1/10th of a mm, then remove all duplicate vertices # This is done to greatly speed up further convex hull calculations as the convex hull # becomes much less complex when dealing with highly detailed models. @@ -325,9 +342,11 @@ class ConvexHullDecorator(SceneNodeDecorator): return convex_hull.getMinkowskiHull(head_and_fans) return None - ## Compensate given 2D polygon with adhesion margin - # \return 2D polygon with added margin def _add2DAdhesionMargin(self, poly: Polygon) -> Polygon: + """Compensate given 2D polygon with adhesion margin + + :return: 2D polygon with added margin + """ if not self._global_stack: return Polygon() # Compensate for raft/skirt/brim @@ -358,12 +377,14 @@ class ConvexHullDecorator(SceneNodeDecorator): poly = poly.getMinkowskiHull(extra_margin_polygon) return poly - ## Offset the convex hull with settings that influence the collision area. - # - # \param convex_hull Polygon of the original convex hull. - # \return New Polygon instance that is offset with everything that - # influences the collision area. def _offsetHull(self, convex_hull: Polygon) -> Polygon: + """Offset the convex hull with settings that influence the collision area. + + :param convex_hull: Polygon of the original convex hull. + :return: New Polygon instance that is offset with everything that + influences the collision area. + """ + horizontal_expansion = max( self._getSettingProperty("xy_offset", "value"), self._getSettingProperty("xy_offset_layer_0", "value") @@ -409,8 +430,9 @@ class ConvexHullDecorator(SceneNodeDecorator): self._onChanged() - ## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property). def _getSettingProperty(self, setting_key: str, prop: str = "value") -> Any: + """Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).""" + if self._global_stack is None or self._node is None: return None per_mesh_stack = self._node.callDecoration("getStack") @@ -430,16 +452,18 @@ class ConvexHullDecorator(SceneNodeDecorator): # Limit_to_extruder is set. The global stack handles this then return self._global_stack.getProperty(setting_key, prop) - ## Returns True if node is a descendant or the same as the root node. def __isDescendant(self, root: "SceneNode", node: Optional["SceneNode"]) -> bool: + """Returns True if node is a descendant or the same as the root node.""" + if node is None: return False if root is node: return True return self.__isDescendant(root, node.getParent()) - ## True if print_sequence is one_at_a_time and _node is not part of a group def _isSingularOneAtATimeNode(self) -> bool: + """True if print_sequence is one_at_a_time and _node is not part of a group""" + if self._node is None: return False return self._global_stack is not None \ @@ -450,7 +474,8 @@ class ConvexHullDecorator(SceneNodeDecorator): "adhesion_type", "raft_margin", "print_sequence", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"] - ## Settings that change the convex hull. - # - # If these settings change, the convex hull should be recalculated. _influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width", "anti_overhang_mesh", "infill_mesh", "cutting_mesh"} + """Settings that change the convex hull. + + If these settings change, the convex hull should be recalculated. + """ diff --git a/cura/Scene/ConvexHullNode.py b/cura/Scene/ConvexHullNode.py index da2713a522..765dae26a2 100644 --- a/cura/Scene/ConvexHullNode.py +++ b/cura/Scene/ConvexHullNode.py @@ -18,11 +18,13 @@ if TYPE_CHECKING: class ConvexHullNode(SceneNode): shader = None # To prevent the shader from being re-built over and over again, only load it once. - ## Convex hull node is a special type of scene node that is used to display an area, to indicate the - # location an object uses on the buildplate. This area (or area's in case of one at a time printing) is - # then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded - # to represent the raft as well. def __init__(self, node: SceneNode, hull: Optional[Polygon], thickness: float, parent: Optional[SceneNode] = None) -> None: + """Convex hull node is a special type of scene node that is used to display an area, to indicate the + + location an object uses on the buildplate. This area (or area's in case of one at a time printing) is + then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded + to represent the raft as well. + """ super().__init__(parent) self.setCalculateBoundingBox(False) diff --git a/cura/Scene/CuraSceneController.py b/cura/Scene/CuraSceneController.py index 36d9e68c8f..99a6eee0e2 100644 --- a/cura/Scene/CuraSceneController.py +++ b/cura/Scene/CuraSceneController.py @@ -6,6 +6,7 @@ from PyQt5.QtWidgets import QApplication from UM.Scene.Camera import Camera from cura.UI.ObjectsModel import ObjectsModel from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel +from cura.Scene.CuraSceneNode import CuraSceneNode from UM.Application import Application from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator @@ -43,6 +44,26 @@ class CuraSceneController(QObject): self._change_timer.start() def updateMaxBuildPlate(self, *args): + global_stack = Application.getInstance().getGlobalContainerStack() + if global_stack: + scene_has_support_meshes = self._sceneHasSupportMeshes() # TODO: see if this can be cached + + if scene_has_support_meshes != global_stack.getProperty("support_meshes_present", "value"): + # Adjust the setting without having the setting value in an InstanceContainer + setting_definitions = global_stack.definition.findDefinitions(key="support_meshes_present") + if setting_definitions: + # Recreate the setting definition because the default_value is readonly + definition_dict = setting_definitions[0].serialize_to_dict() + definition_dict["enabled"] = False # The enabled property has a value that would need to be evaluated + definition_dict["default_value"] = scene_has_support_meshes + relations = setting_definitions[0].relations # Relations are wiped when deserializing from a dict + setting_definitions[0].deserialize(definition_dict) + + # Restore relations and notify them that the setting has changed + for relation in relations: + setting_definitions[0].relations.append(relation) + global_stack.propertyChanged.emit(relation.target.key, "enabled") + max_build_plate = self._calcMaxBuildPlate() changed = False if max_build_plate != self._max_build_plate: @@ -72,9 +93,19 @@ class CuraSceneController(QObject): max_build_plate = max(build_plate_number, max_build_plate) return max_build_plate - ## Either select or deselect an item + def _sceneHasSupportMeshes(self): + root = Application.getInstance().getController().getScene().getRoot() + for node in root.getAllChildren(): + if isinstance(node, CuraSceneNode): + per_mesh_stack = node.callDecoration("getStack") + if per_mesh_stack and per_mesh_stack.getProperty("support_mesh", "value"): + return True + return False + @pyqtSlot(int) def changeSelection(self, index): + """Either select or deselect an item""" + modifiers = QApplication.keyboardModifiers() ctrl_is_active = modifiers & Qt.ControlModifier shift_is_active = modifiers & Qt.ShiftModifier diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index eb609def5a..c22277a4b0 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -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. from copy import deepcopy @@ -15,9 +15,11 @@ from cura.Settings.ExtruderStack import ExtruderStack # For typing. from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings. -## Scene nodes that are models are only seen when selecting the corresponding build plate -# Note that many other nodes can just be UM SceneNode objects. class CuraSceneNode(SceneNode): + """Scene nodes that are models are only seen when selecting the corresponding build plate + + Note that many other nodes can just be UM SceneNode objects. + """ def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None: super().__init__(parent = parent, visible = visible, name = name) if not no_setting_override: @@ -36,15 +38,23 @@ class CuraSceneNode(SceneNode): def isSelectable(self) -> bool: return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate - ## Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned - # TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded + def isSupportMesh(self) -> bool: + per_mesh_stack = self.callDecoration("getStack") + if not per_mesh_stack: + return False + return per_mesh_stack.getProperty("support_mesh", "value") + def getPrintingExtruder(self) -> Optional[ExtruderStack]: + """Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned + + TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded + """ global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack is None: return None per_mesh_stack = self.callDecoration("getStack") - extruders = list(global_container_stack.extruders.values()) + extruders = global_container_stack.extruderList # Use the support extruder instead of the active extruder if this is a support_mesh if per_mesh_stack: @@ -69,8 +79,9 @@ class CuraSceneNode(SceneNode): # This point should never be reached return None - ## Return the color of the material used to print this model def getDiffuseColor(self) -> List[float]: + """Return the color of the material used to print this model""" + printing_extruder = self.getPrintingExtruder() material_color = "#808080" # Fallback color @@ -86,8 +97,9 @@ class CuraSceneNode(SceneNode): 1.0 ] - ## Return if any area collides with the convex hull of this scene node def collidesWithAreas(self, areas: List[Polygon]) -> bool: + """Return if any area collides with the convex hull of this scene node""" + convex_hull = self.callDecoration("getPrintingArea") if convex_hull: if not convex_hull.isValid(): @@ -101,14 +113,15 @@ class CuraSceneNode(SceneNode): return True return False - ## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box def _calculateAABB(self) -> None: + """Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box""" + self._aabb = None if self._mesh_data: self._aabb = self._mesh_data.getExtents(self.getWorldTransformation()) - else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0) + else: # If there is no mesh_data, use a bounding box that encompasses the local (0,0,0) position = self.getWorldPosition() - self._aabb = AxisAlignedBox(minimum=position, maximum=position) + self._aabb = AxisAlignedBox(minimum = position, maximum = position) for child in self.getAllChildren(): if child.callDecoration("isNonPrintingMesh"): @@ -122,8 +135,9 @@ class CuraSceneNode(SceneNode): else: self._aabb = self._aabb + child.getBoundingBox() - ## Taken from SceneNode, but replaced SceneNode with CuraSceneNode def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode": + """Taken from SceneNode, but replaced SceneNode with CuraSceneNode""" + copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later copy.setTransformation(self.getLocalTransformation()) copy.setMeshData(self._mesh_data) diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index 982a38d667..ad51f7d755 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -4,7 +4,7 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator class SliceableObjectDecorator(SceneNodeDecorator): def __init__(self) -> None: super().__init__() - + def isSliceable(self) -> bool: return True diff --git a/cura/Scene/ZOffsetDecorator.py b/cura/Scene/ZOffsetDecorator.py index b35b17a412..1f1f5a9b1f 100644 --- a/cura/Scene/ZOffsetDecorator.py +++ b/cura/Scene/ZOffsetDecorator.py @@ -1,8 +1,9 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator -## A decorator that stores the amount an object has been moved below the platform. class ZOffsetDecorator(SceneNodeDecorator): + """A decorator that stores the amount an object has been moved below the platform.""" + def __init__(self) -> None: super().__init__() self._z_offset = 0. diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 4d972ba87e..09a6bb5bb6 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -33,12 +33,14 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## Manager class that contains common actions to deal with containers in Cura. -# -# This is primarily intended as a class to be able to perform certain actions -# from within QML. We want to be able to trigger things like removing a container -# when a certain action happens. This can be done through this class. class ContainerManager(QObject): + """Manager class that contains common actions to deal with containers in Cura. + + This is primarily intended as a class to be able to perform certain actions + from within QML. We want to be able to trigger things like removing a container + when a certain action happens. This can be done through this class. + """ + def __init__(self, application: "CuraApplication") -> None: if ContainerManager.__instance is not None: @@ -67,21 +69,23 @@ class ContainerManager(QObject): return "" return str(result) - ## Set a metadata entry of the specified container. - # - # This will set the specified entry of the container's metadata to the specified - # value. Note that entries containing dictionaries can have their entries changed - # by using "/" as a separator. For example, to change an entry "foo" in a - # dictionary entry "bar", you can specify "bar/foo" as entry name. - # - # \param container_node \type{ContainerNode} - # \param entry_name \type{str} The name of the metadata entry to change. - # \param entry_value The new value of the entry. - # - # TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this. - # Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want? @pyqtSlot("QVariant", str, str) def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool: + """Set a metadata entry of the specified container. + + This will set the specified entry of the container's metadata to the specified + value. Note that entries containing dictionaries can have their entries changed + by using "/" as a separator. For example, to change an entry "foo" in a + dictionary entry "bar", you can specify "bar/foo" as entry name. + + :param container_node: :type{ContainerNode} + :param entry_name: :type{str} The name of the metadata entry to change. + :param entry_value: The new value of the entry. + + TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this. + Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want? + """ + if container_node.container is None: Logger.log("w", "Container node {0} doesn't have a container.".format(container_node.container_id)) return False @@ -124,18 +128,20 @@ class ContainerManager(QObject): def makeUniqueName(self, original_name: str) -> str: return cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().uniqueName(original_name) - ## Get a list of string that can be used as name filters for a Qt File Dialog - # - # This will go through the list of available container types and generate a list of strings - # out of that. The strings are formatted as "description (*.extension)" and can be directly - # passed to a nameFilters property of a Qt File Dialog. - # - # \param type_name Which types of containers to list. These types correspond to the "type" - # key of the plugin metadata. - # - # \return A string list with name filters. @pyqtSlot(str, result = "QStringList") def getContainerNameFilters(self, type_name: str) -> List[str]: + """Get a list of string that can be used as name filters for a Qt File Dialog + + This will go through the list of available container types and generate a list of strings + out of that. The strings are formatted as "description (*.extension)" and can be directly + passed to a nameFilters property of a Qt File Dialog. + + :param type_name: Which types of containers to list. These types correspond to the "type" + key of the plugin metadata. + + :return: A string list with name filters. + """ + if not self._container_name_filters: self._updateContainerNameFilters() @@ -147,17 +153,18 @@ class ContainerManager(QObject): filters.append("All Files (*)") return filters - ## Export a container to a file - # - # \param container_id The ID of the container to export - # \param file_type The type of file to save as. Should be in the form of "description (*.extension, *.ext)" - # \param file_url_or_string The URL where to save the file. - # - # \return A dictionary containing a key "status" with a status code and a key "message" with a message - # explaining the status. - # The status code can be one of "error", "cancelled", "success" @pyqtSlot(str, str, QUrl, result = "QVariantMap") def exportContainer(self, container_id: str, file_type: str, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]: + """Export a container to a file + + :param container_id: The ID of the container to export + :param file_type: The type of file to save as. Should be in the form of "description (*.extension, *.ext)" + :param file_url_or_string: The URL where to save the file. + + :return: A dictionary containing a key "status" with a status code and a key "message" with a message + explaining the status. The status code can be one of "error", "cancelled", "success" + """ + if not container_id or not file_type or not file_url_or_string: return {"status": "error", "message": "Invalid arguments"} @@ -214,14 +221,16 @@ class ContainerManager(QObject): return {"status": "success", "message": "Successfully exported container", "path": file_url} - ## Imports a profile from a file - # - # \param file_url A URL that points to the file to import. - # - # \return \type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key - # containing a message for the user @pyqtSlot(QUrl, result = "QVariantMap") def importMaterialContainer(self, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]: + """Imports a profile from a file + + :param file_url: A URL that points to the file to import. + + :return: :type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key + containing a message for the user + """ + if not file_url_or_string: return {"status": "error", "message": "Invalid path"} @@ -266,14 +275,16 @@ class ContainerManager(QObject): return {"status": "success", "message": "Successfully imported container {0}".format(container.getName())} - ## Update the current active quality changes container with the settings from the user container. - # - # This will go through the active global stack and all active extruder stacks and merge the changes from the user - # container into the quality_changes container. After that, the user container is cleared. - # - # \return \type{bool} True if successful, False if not. @pyqtSlot(result = bool) def updateQualityChanges(self) -> bool: + """Update the current active quality changes container with the settings from the user container. + + This will go through the active global stack and all active extruder stacks and merge the changes from the user + container into the quality_changes container. After that, the user container is cleared. + + :return: :type{bool} True if successful, False if not. + """ + application = cura.CuraApplication.CuraApplication.getInstance() global_stack = application.getMachineManager().activeMachine if not global_stack: @@ -283,7 +294,7 @@ class ContainerManager(QObject): current_quality_changes_name = global_stack.qualityChanges.getName() current_quality_type = global_stack.quality.getMetaDataEntry("quality_type") - extruder_stacks = list(global_stack.extruders.values()) + extruder_stacks = global_stack.extruderList container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry() machine_definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition for stack in [global_stack] + extruder_stacks: @@ -313,9 +324,10 @@ class ContainerManager(QObject): return True - ## Clear the top-most (user) containers of the active stacks. @pyqtSlot() def clearUserContainers(self) -> None: + """Clear the top-most (user) containers of the active stacks.""" + machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager() machine_manager.blurSettings.emit() @@ -323,8 +335,7 @@ class ContainerManager(QObject): # Go through global and extruder stacks and clear their topmost container (the user settings). global_stack = machine_manager.activeMachine - extruder_stacks = list(global_stack.extruders.values()) - for stack in [global_stack] + extruder_stacks: + for stack in [global_stack] + global_stack.extruderList: container = stack.userChanges container.clear() send_emits_containers.append(container) @@ -335,25 +346,28 @@ class ContainerManager(QObject): for container in send_emits_containers: container.sendPostponedEmits() - ## Get a list of materials that have the same GUID as the reference material - # - # \param material_node The node representing the material for which to get - # the same GUID. - # \param exclude_self Whether to include the name of the material you - # provided. - # \return A list of names of materials with the same GUID. @pyqtSlot("QVariant", bool, result = "QStringList") def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False) -> List[str]: + """Get a list of materials that have the same GUID as the reference material + + :param material_node: The node representing the material for which to get + the same GUID. + :param exclude_self: Whether to include the name of the material you provided. + :return: A list of names of materials with the same GUID. + """ + same_guid = ContainerRegistry.getInstance().findInstanceContainersMetadata(GUID = material_node.guid) if exclude_self: return list({meta["name"] for meta in same_guid if meta["base_file"] != material_node.base_file}) else: return list({meta["name"] for meta in same_guid}) - ## Unlink a material from all other materials by creating a new GUID - # \param material_id \type{str} the id of the material to create a new GUID for. @pyqtSlot("QVariant") def unlinkMaterial(self, material_node: "MaterialNode") -> None: + """Unlink a material from all other materials by creating a new GUID + + :param material_id: :type{str} the id of the material to create a new GUID for. + """ # Get the material group if material_node.container is None: # Failed to lazy-load this container. return @@ -428,9 +442,10 @@ class ContainerManager(QObject): name_filter = "{0} ({1})".format(mime_type.comment, suffix_list) self._container_name_filters[name_filter] = entry - ## Import single profile, file_url does not have to end with curaprofile @pyqtSlot(QUrl, result = "QVariantMap") def importProfile(self, file_url: QUrl) -> Dict[str, str]: + """Import single profile, file_url does not have to end with curaprofile""" + if not file_url.isValid(): return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)} path = file_url.toLocalFile() diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 0ef09a1fac..a0cfd61e49 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -1,765 +1,782 @@ -# Copyright (c) 2019 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import os -import re -import configparser - -from typing import Any, cast, Dict, Optional, List, Union -from PyQt5.QtWidgets import QMessageBox - -from UM.Decorators import override -from UM.Settings.ContainerFormatError import ContainerFormatError -from UM.Settings.Interfaces import ContainerInterface -from UM.Settings.ContainerRegistry import ContainerRegistry -from UM.Settings.ContainerStack import ContainerStack -from UM.Settings.InstanceContainer import InstanceContainer -from UM.Settings.SettingInstance import SettingInstance -from UM.Logger import Logger -from UM.Message import Message -from UM.Platform import Platform -from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with. -from UM.Resources import Resources -from UM.Util import parseBool -from cura.ReaderWriters.ProfileWriter import ProfileWriter - -from . import ExtruderStack -from . import GlobalStack - -import cura.CuraApplication -from cura.Settings.cura_empty_instance_containers import empty_quality_container -from cura.Machines.ContainerTree import ContainerTree -from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader - -from UM.i18n import i18nCatalog -catalog = i18nCatalog("cura") - - -class CuraContainerRegistry(ContainerRegistry): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack - # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack - # is added, we check to see if an extruder stack needs to be added. - self.containerAdded.connect(self._onContainerAdded) - - ## Overridden from ContainerRegistry - # - # Adds a container to the registry. - # - # This will also try to convert a ContainerStack to either Extruder or - # Global stack based on metadata information. - @override(ContainerRegistry) - def addContainer(self, container: ContainerInterface) -> None: - # Note: Intentional check with type() because we want to ignore subclasses - if type(container) == ContainerStack: - container = self._convertContainerStack(cast(ContainerStack, container)) - - if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()): - # Check against setting version of the definition. - required_setting_version = cura.CuraApplication.CuraApplication.SettingVersion - actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0)) - if required_setting_version != actual_setting_version: - Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version)) - return # Don't add. - - super().addContainer(container) - - ## Create a name that is not empty and unique - # \param container_type \type{string} Type of the container (machine, quality, ...) - # \param current_name \type{} Current name of the container, which may be an acceptable option - # \param new_name \type{string} Base name, which may not be unique - # \param fallback_name \type{string} Name to use when (stripped) new_name is empty - # \return \type{string} Name that is unique for the specified type and name/id - def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str: - new_name = new_name.strip() - num_check = re.compile(r"(.*?)\s*#\d+$").match(new_name) - if num_check: - new_name = num_check.group(1) - if new_name == "": - new_name = fallback_name - - unique_name = new_name - i = 1 - # In case we are renaming, the current name of the container is also a valid end-result - while self._containerExists(container_type, unique_name) and unique_name != current_name: - i += 1 - unique_name = "%s #%d" % (new_name, i) - - return unique_name - - ## Check if a container with of a certain type and a certain name or id exists - # Both the id and the name are checked, because they may not be the same and it is better if they are both unique - # \param container_type \type{string} Type of the container (machine, quality, ...) - # \param container_name \type{string} Name to check - def _containerExists(self, container_type: str, container_name: str): - container_class = ContainerStack if container_type == "machine" else InstanceContainer - - return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \ - self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type) - - ## Exports an profile to a file - # - # \param container_list \type{list} the containers to export. This is not - # necessarily in any order! - # \param file_name \type{str} the full path and filename to export to. - # \param file_type \type{str} the file type with the format " (*.)" - # \return True if the export succeeded, false otherwise. - def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool: - # Parse the fileType to deduce what plugin can save the file format. - # fileType has the format " (*.)" - split = file_type.rfind(" (*.") # Find where the description ends and the extension starts. - if split < 0: # Not found. Invalid format. - Logger.log("e", "Invalid file format identifier %s", file_type) - return False - description = file_type[:split] - extension = file_type[split + 4:-1] # Leave out the " (*." and ")". - if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any. - file_name += "." + extension - - # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself. - if not Platform.isWindows(): - if os.path.exists(file_name): - result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"), - catalog.i18nc("@label Don't translate the XML tag !", "The file {0} already exists. Are you sure you want to overwrite it?").format(file_name)) - if result == QMessageBox.No: - return False - - profile_writer = self._findProfileWriter(extension, description) - try: - if profile_writer is None: - raise Exception("Unable to find a profile writer") - success = profile_writer.write(file_name, container_list) - except Exception as e: - Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e)) - m = Message(catalog.i18nc("@info:status Don't translate the XML tags or !", "Failed to export profile to {0}: {1}", file_name, str(e)), - lifetime = 0, - title = catalog.i18nc("@info:title", "Error")) - m.show() - return False - if not success: - Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name) - m = Message(catalog.i18nc("@info:status Don't translate the XML tag !", "Failed to export profile to {0}: Writer plugin reported failure.", file_name), - lifetime = 0, - title = catalog.i18nc("@info:title", "Error")) - m.show() - return False - m = Message(catalog.i18nc("@info:status Don't translate the XML tag !", "Exported profile to {0}", file_name), - title = catalog.i18nc("@info:title", "Export succeeded")) - m.show() - return True - - ## Gets the plugin object matching the criteria - # \param extension - # \param description - # \return The plugin object matching the given extension and description. - def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]: - plugin_registry = PluginRegistry.getInstance() - for plugin_id, meta_data in self._getIOPlugins("profile_writer"): - for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write. - supported_extension = supported_type.get("extension", None) - if supported_extension == extension: # This plugin supports a file type with the same extension. - supported_description = supported_type.get("description", None) - if supported_description == description: # The description is also identical. Assume it's the same file type. - return cast(ProfileWriter, plugin_registry.getPluginObject(plugin_id)) - return None - - ## Imports a profile from a file - # - # \param file_name The full path and filename of the profile to import. - # \return Dict with a 'status' key containing the string 'ok' or 'error', - # and a 'message' key containing a message for the user. - def importProfile(self, file_name: str) -> Dict[str, str]: - Logger.log("d", "Attempting to import profile %s", file_name) - if not file_name: - return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Failed to import profile from {0}: {1}", file_name, "Invalid path")} - - global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() - if not global_stack: - return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Can't import profile from {0} before a printer is added.", file_name)} - container_tree = ContainerTree.getInstance() - - machine_extruders = [] - for position in sorted(global_stack.extruders): - machine_extruders.append(global_stack.extruders[position]) - - plugin_registry = PluginRegistry.getInstance() - extension = file_name.split(".")[-1] - - for plugin_id, meta_data in self._getIOPlugins("profile_reader"): - if meta_data["profile_reader"][0]["extension"] != extension: - continue - profile_reader = cast(ProfileReader, plugin_registry.getPluginObject(plugin_id)) - try: - profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader. - except NoProfileException: - return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "No custom profile to import in file {0}", file_name)} - except Exception as e: - # Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None. - Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e)) - return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Failed to import profile from {0}:", file_name) + "\n" + str(e) + ""} - - if profile_or_list: - # Ensure it is always a list of profiles - if not isinstance(profile_or_list, list): - profile_or_list = [profile_or_list] - - # First check if this profile is suitable for this machine - global_profile = None - extruder_profiles = [] - if len(profile_or_list) == 1: - global_profile = profile_or_list[0] - else: - for profile in profile_or_list: - if not profile.getMetaDataEntry("position"): - global_profile = profile - else: - extruder_profiles.append(profile) - extruder_profiles = sorted(extruder_profiles, key = lambda x: int(x.getMetaDataEntry("position"))) - profile_or_list = [global_profile] + extruder_profiles - - if not global_profile: - Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name) - return { "status": "error", - "message": catalog.i18nc("@info:status Don't translate the XML tags !", "This profile {0} contains incorrect data, could not import it.", file_name)} - profile_definition = global_profile.getMetaDataEntry("definition") - - # Make sure we have a profile_definition in the file: - if profile_definition is None: - break - machine_definitions = self.findContainers(id = profile_definition) - if not machine_definitions: - Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition) - return {"status": "error", - "message": catalog.i18nc("@info:status Don't translate the XML tags !", "This profile {0} contains incorrect data, could not import it.", file_name) - } - machine_definition = machine_definitions[0] - - # Get the expected machine definition. - # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... - has_machine_quality = parseBool(machine_definition.getMetaDataEntry("has_machine_quality", "false")) - profile_definition = machine_definition.getMetaDataEntry("quality_definition", machine_definition.getId()) if has_machine_quality else "fdmprinter" - expected_machine_definition = container_tree.machines[global_stack.definition.getId()].quality_definition - - # And check if the profile_definition matches either one (showing error if not): - if profile_definition != expected_machine_definition: - Logger.log("d", "Profile {file_name} is for machine {profile_definition}, but the current active machine is {expected_machine_definition}. Changing profile's definition.".format(file_name = file_name, profile_definition = profile_definition, expected_machine_definition = expected_machine_definition)) - global_profile.setMetaDataEntry("definition", expected_machine_definition) - for extruder_profile in extruder_profiles: - extruder_profile.setMetaDataEntry("definition", expected_machine_definition) - - quality_name = global_profile.getName() - quality_type = global_profile.getMetaDataEntry("quality_type") - - name_seed = os.path.splitext(os.path.basename(file_name))[0] - new_name = self.uniqueName(name_seed) - - # Ensure it is always a list of profiles - if type(profile_or_list) is not list: - profile_or_list = [profile_or_list] - - # Make sure that there are also extruder stacks' quality_changes, not just one for the global stack - if len(profile_or_list) == 1: - global_profile = profile_or_list[0] - extruder_profiles = [] - for idx, extruder in enumerate(global_stack.extruders.values()): - profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1)) - profile = InstanceContainer(profile_id) - profile.setName(quality_name) - profile.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion) - profile.setMetaDataEntry("type", "quality_changes") - profile.setMetaDataEntry("definition", expected_machine_definition) - profile.setMetaDataEntry("quality_type", quality_type) - profile.setDirty(True) - if idx == 0: - # Move all per-extruder settings to the first extruder's quality_changes - for qc_setting_key in global_profile.getAllKeys(): - settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder") - if settable_per_extruder: - setting_value = global_profile.getProperty(qc_setting_key, "value") - - setting_definition = global_stack.getSettingDefinition(qc_setting_key) - if setting_definition is not None: - new_instance = SettingInstance(setting_definition, profile) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - profile.addInstance(new_instance) - profile.setDirty(True) - - global_profile.removeInstance(qc_setting_key, postpone_emit = True) - extruder_profiles.append(profile) - - for profile in extruder_profiles: - profile_or_list.append(profile) - - # Import all profiles - profile_ids_added = [] # type: List[str] - for profile_index, profile in enumerate(profile_or_list): - if profile_index == 0: - # This is assumed to be the global profile - profile_id = (cast(ContainerInterface, global_stack.getBottom()).getId() + "_" + name_seed).lower().replace(" ", "_") - - elif profile_index < len(machine_extruders) + 1: - # This is assumed to be an extruder profile - extruder_id = machine_extruders[profile_index - 1].definition.getId() - extruder_position = str(profile_index - 1) - if not profile.getMetaDataEntry("position"): - profile.setMetaDataEntry("position", extruder_position) - else: - profile.setMetaDataEntry("position", extruder_position) - profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_") - - else: # More extruders in the imported file than in the machine. - continue # Delete the additional profiles. - - result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition) - if result is not None: - # Remove any profiles that did got added. - for profile_id in profile_ids_added: - self.removeContainer(profile_id) - - return {"status": "error", "message": catalog.i18nc( - "@info:status Don't translate the XML tag !", - "Failed to import profile from {0}:", - file_name) + " " + result} - profile_ids_added.append(profile.getId()) - return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} - - # This message is throw when the profile reader doesn't find any profile in the file - return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)} - - # If it hasn't returned by now, none of the plugins loaded the profile successfully. - return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)} - - @override(ContainerRegistry) - def load(self) -> None: - super().load() - self._registerSingleExtrusionMachinesExtruderStacks() - self._connectUpgradedExtruderStacksToMachines() - - ## Check if the metadata for a container is okay before adding it. - # - # This overrides the one from UM.Settings.ContainerRegistry because we - # also require that the setting_version is correct. - @override(ContainerRegistry) - def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool: - if metadata is None: - return False - if "setting_version" not in metadata: - return False - try: - if int(metadata["setting_version"]) != cura.CuraApplication.CuraApplication.SettingVersion: - return False - except ValueError: #Not parsable as int. - return False - return True - - ## Update an imported profile to match the current machine configuration. - # - # \param profile The profile to configure. - # \param id_seed The base ID for the profile. May be changed so it does not conflict with existing containers. - # \param new_name The new name for the profile. - # - # \return None if configuring was successful or an error message if an error occurred. - def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]: - profile.setDirty(True) # Ensure the profiles are correctly saved - - new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile")) - profile.setMetaDataEntry("id", new_id) - profile.setName(new_name) - - # Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile - # It also solves an issue with importing profiles from G-Codes - profile.setMetaDataEntry("id", new_id) - profile.setMetaDataEntry("definition", machine_definition_id) - - if "type" in profile.getMetaData(): - profile.setMetaDataEntry("type", "quality_changes") - else: - profile.setMetaDataEntry("type", "quality_changes") - - quality_type = profile.getMetaDataEntry("quality_type") - if not quality_type: - return catalog.i18nc("@info:status", "Profile is missing a quality type.") - - global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() - if global_stack is None: - return None - definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition - profile.setDefinition(definition_id) - - # Check to make sure the imported profile actually makes sense in context of the current configuration. - # This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as - # successfully imported but then fail to show up. - quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups() - # "not_supported" profiles can be imported. - if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict: - return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type) - - ContainerRegistry.getInstance().addContainer(profile) - - return None - - @override(ContainerRegistry) - def saveDirtyContainers(self) -> None: - # Lock file for "more" atomically loading and saving to/from config dir. - with self.lockFile(): - # Save base files first - for instance in self.findDirtyContainers(container_type=InstanceContainer): - if instance.getMetaDataEntry("removed"): - continue - if instance.getId() == instance.getMetaData().get("base_file"): - self.saveContainer(instance) - - for instance in self.findDirtyContainers(container_type=InstanceContainer): - if instance.getMetaDataEntry("removed"): - continue - self.saveContainer(instance) - - for stack in self.findContainerStacks(): - self.saveContainer(stack) - - ## Gets a list of profile writer plugins - # \return List of tuples of (plugin_id, meta_data). - def _getIOPlugins(self, io_type): - plugin_registry = PluginRegistry.getInstance() - active_plugin_ids = plugin_registry.getActivePlugins() - - result = [] - for plugin_id in active_plugin_ids: - meta_data = plugin_registry.getMetaData(plugin_id) - if io_type in meta_data: - result.append( (plugin_id, meta_data) ) - return result - - ## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack. - def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]: - assert type(container) == ContainerStack - - container_type = container.getMetaDataEntry("type") - if container_type not in ("extruder_train", "machine"): - # It is not an extruder or machine, so do nothing with the stack - return container - - Logger.log("d", "Converting ContainerStack {stack} to {type}", stack = container.getId(), type = container_type) - - if container_type == "extruder_train": - new_stack = ExtruderStack.ExtruderStack(container.getId()) - else: - new_stack = GlobalStack.GlobalStack(container.getId()) - - container_contents = container.serialize() - new_stack.deserialize(container_contents) - - # Delete the old configuration file so we do not get double stacks - if os.path.isfile(container.getPath()): - os.remove(container.getPath()) - - return new_stack - - def _registerSingleExtrusionMachinesExtruderStacks(self) -> None: - machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"}) - for machine in machines: - extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId()) - if not extruder_stacks: - self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder") - - def _onContainerAdded(self, container: ContainerInterface) -> None: - # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack - # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack - # is added, we check to see if an extruder stack needs to be added. - if not isinstance(container, ContainerStack) or container.getMetaDataEntry("type") != "machine": - return - - machine_extruder_trains = container.getMetaDataEntry("machine_extruder_trains") - if machine_extruder_trains is not None and machine_extruder_trains != {"0": "fdmextruder"}: - return - - extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = container.getId()) - if not extruder_stacks: - self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder") - - # - # new_global_quality_changes is optional. It is only used in project loading for a scenario like this: - # - override the current machine - # - create new for custom quality profile - # new_global_quality_changes is the new global quality changes container in this scenario. - # create_new_ids indicates if new unique ids must be created - # - def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None, create_new_ids = True): - new_extruder_id = extruder_id - - application = cura.CuraApplication.CuraApplication.getInstance() - - extruder_definitions = self.findDefinitionContainers(id = new_extruder_id) - if not extruder_definitions: - Logger.log("w", "Could not find definition containers for extruder %s", new_extruder_id) - return - - extruder_definition = extruder_definitions[0] - unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) if create_new_ids else machine.getName() + " " + new_extruder_id - - extruder_stack = ExtruderStack.ExtruderStack(unique_name) - extruder_stack.setName(extruder_definition.getName()) - extruder_stack.setDefinition(extruder_definition) - extruder_stack.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) - - # create a new definition_changes container for the extruder stack - definition_changes_id = self.uniqueName(extruder_stack.getId() + "_settings") if create_new_ids else extruder_stack.getId() + "_settings" - definition_changes_name = definition_changes_id - definition_changes = InstanceContainer(definition_changes_id, parent = application) - definition_changes.setName(definition_changes_name) - definition_changes.setMetaDataEntry("setting_version", application.SettingVersion) - definition_changes.setMetaDataEntry("type", "definition_changes") - definition_changes.setMetaDataEntry("definition", extruder_definition.getId()) - - # move definition_changes settings if exist - for setting_key in definition_changes.getAllKeys(): - if machine.definition.getProperty(setting_key, "settable_per_extruder"): - setting_value = machine.definitionChanges.getProperty(setting_key, "value") - if setting_value is not None: - # move it to the extruder stack's definition_changes - setting_definition = machine.getSettingDefinition(setting_key) - new_instance = SettingInstance(setting_definition, definition_changes) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - definition_changes.addInstance(new_instance) - definition_changes.setDirty(True) - - machine.definitionChanges.removeInstance(setting_key, postpone_emit = True) - - self.addContainer(definition_changes) - extruder_stack.setDefinitionChanges(definition_changes) - - # create empty user changes container otherwise - user_container_id = self.uniqueName(extruder_stack.getId() + "_user") if create_new_ids else extruder_stack.getId() + "_user" - user_container_name = user_container_id - user_container = InstanceContainer(user_container_id, parent = application) - user_container.setName(user_container_name) - user_container.setMetaDataEntry("type", "user") - user_container.setMetaDataEntry("machine", machine.getId()) - user_container.setMetaDataEntry("setting_version", application.SettingVersion) - user_container.setDefinition(machine.definition.getId()) - user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position")) - - if machine.userChanges: - # For the newly created extruder stack, we need to move all "per-extruder" settings to the user changes - # container to the extruder stack. - for user_setting_key in machine.userChanges.getAllKeys(): - settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder") - if settable_per_extruder: - setting_value = machine.getProperty(user_setting_key, "value") - - setting_definition = machine.getSettingDefinition(user_setting_key) - new_instance = SettingInstance(setting_definition, definition_changes) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - user_container.addInstance(new_instance) - user_container.setDirty(True) - - machine.userChanges.removeInstance(user_setting_key, postpone_emit = True) - - self.addContainer(user_container) - extruder_stack.setUserChanges(user_container) - - empty_variant = application.empty_variant_container - empty_material = application.empty_material_container - empty_quality = application.empty_quality_container - - if machine.variant.getId() not in ("empty", "empty_variant"): - variant = machine.variant - else: - variant = empty_variant - extruder_stack.variant = variant - - if machine.material.getId() not in ("empty", "empty_material"): - material = machine.material - else: - material = empty_material - extruder_stack.material = material - - if machine.quality.getId() not in ("empty", "empty_quality"): - quality = machine.quality - else: - quality = empty_quality - extruder_stack.quality = quality - - machine_quality_changes = machine.qualityChanges - if new_global_quality_changes is not None: - machine_quality_changes = new_global_quality_changes - - if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): - extruder_quality_changes_container = self.findInstanceContainers(name = machine_quality_changes.getName(), extruder = extruder_id) - if extruder_quality_changes_container: - extruder_quality_changes_container = extruder_quality_changes_container[0] - - quality_changes_id = extruder_quality_changes_container.getId() - extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] - else: - # Some extruder quality_changes containers can be created at runtime as files in the qualities - # folder. Those files won't be loaded in the registry immediately. So we also need to search - # the folder to see if the quality_changes exists. - extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) - if extruder_quality_changes_container: - quality_changes_id = extruder_quality_changes_container.getId() - extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) - extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] - else: - # If we still cannot find a quality changes container for the extruder, create a new one - container_name = machine_quality_changes.getName() - container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name) - extruder_quality_changes_container = InstanceContainer(container_id, parent = application) - extruder_quality_changes_container.setName(container_name) - extruder_quality_changes_container.setMetaDataEntry("type", "quality_changes") - extruder_quality_changes_container.setMetaDataEntry("setting_version", application.SettingVersion) - extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) - extruder_quality_changes_container.setMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type")) - extruder_quality_changes_container.setMetaDataEntry("intent_category", "default") # Intent categories weren't a thing back then. - extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId()) - - self.addContainer(extruder_quality_changes_container) - extruder_stack.qualityChanges = extruder_quality_changes_container - - if not extruder_quality_changes_container: - Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", - machine_quality_changes.getName(), extruder_stack.getId()) - else: - # Move all per-extruder settings to the extruder's quality changes - for qc_setting_key in machine_quality_changes.getAllKeys(): - settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") - if settable_per_extruder: - setting_value = machine_quality_changes.getProperty(qc_setting_key, "value") - - setting_definition = machine.getSettingDefinition(qc_setting_key) - new_instance = SettingInstance(setting_definition, definition_changes) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - extruder_quality_changes_container.addInstance(new_instance) - extruder_quality_changes_container.setDirty(True) - - machine_quality_changes.removeInstance(qc_setting_key, postpone_emit=True) - else: - extruder_stack.qualityChanges = self.findInstanceContainers(id = "empty_quality_changes")[0] - - self.addContainer(extruder_stack) - - # Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have - # per-extruder settings in the container for the machine instead of the extruder. - if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): - quality_changes_machine_definition_id = machine_quality_changes.getDefinition().getId() - else: - whole_machine_definition = machine.definition - machine_entry = machine.definition.getMetaDataEntry("machine") - if machine_entry is not None: - container_registry = ContainerRegistry.getInstance() - whole_machine_definition = container_registry.findDefinitionContainers(id = machine_entry)[0] - - quality_changes_machine_definition_id = "fdmprinter" - if whole_machine_definition.getMetaDataEntry("has_machine_quality"): - quality_changes_machine_definition_id = machine.definition.getMetaDataEntry("quality_definition", - whole_machine_definition.getId()) - qcs = self.findInstanceContainers(type = "quality_changes", definition = quality_changes_machine_definition_id) - qc_groups = {} # map of qc names -> qc containers - for qc in qcs: - qc_name = qc.getName() - if qc_name not in qc_groups: - qc_groups[qc_name] = [] - qc_groups[qc_name].append(qc) - # Try to find from the quality changes cura directory too - quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) - if quality_changes_container: - qc_groups[qc_name].append(quality_changes_container) - - for qc_name, qc_list in qc_groups.items(): - qc_dict = {"global": None, "extruders": []} - for qc in qc_list: - extruder_position = qc.getMetaDataEntry("position") - if extruder_position is not None: - qc_dict["extruders"].append(qc) - else: - qc_dict["global"] = qc - if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1: - # Move per-extruder settings - for qc_setting_key in qc_dict["global"].getAllKeys(): - settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") - if settable_per_extruder: - setting_value = qc_dict["global"].getProperty(qc_setting_key, "value") - - setting_definition = machine.getSettingDefinition(qc_setting_key) - new_instance = SettingInstance(setting_definition, definition_changes) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - qc_dict["extruders"][0].addInstance(new_instance) - qc_dict["extruders"][0].setDirty(True) - - qc_dict["global"].removeInstance(qc_setting_key, postpone_emit=True) - - # Set next stack at the end - extruder_stack.setNextStack(machine) - - return extruder_stack - - def _findQualityChangesContainerInCuraFolder(self, name: str) -> Optional[InstanceContainer]: - quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer) - - instance_container = None - - for item in os.listdir(quality_changes_dir): - file_path = os.path.join(quality_changes_dir, item) - if not os.path.isfile(file_path): - continue - - parser = configparser.ConfigParser(interpolation = None) - try: - parser.read([file_path]) - except Exception: - # Skip, it is not a valid stack file - continue - - if not parser.has_option("general", "name"): - continue - - if parser["general"]["name"] == name: - # Load the container - container_id = os.path.basename(file_path).replace(".inst.cfg", "") - if self.findInstanceContainers(id = container_id): - # This container is already in the registry, skip it - continue - - instance_container = InstanceContainer(container_id) - with open(file_path, "r", encoding = "utf-8") as f: - serialized = f.read() - try: - instance_container.deserialize(serialized, file_path) - except ContainerFormatError: - Logger.logException("e", "Unable to deserialize InstanceContainer %s", file_path) - continue - self.addContainer(instance_container) - break - - return instance_container - - # Fix the extruders that were upgraded to ExtruderStack instances during addContainer. - # The stacks are now responsible for setting the next stack on deserialize. However, - # due to problems with loading order, some stacks may not have the proper next stack - # set after upgrading, because the proper global stack was not yet loaded. This method - # makes sure those extruders also get the right stack set. - def _connectUpgradedExtruderStacksToMachines(self) -> None: - extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack) - for extruder_stack in extruder_stacks: - if extruder_stack.getNextStack(): - # Has the right next stack, so ignore it. - continue - - machines = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack.getMetaDataEntry("machine", "")) - if machines: - extruder_stack.setNextStack(machines[0]) - else: - Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId()) - - # Override just for the type. - @classmethod - @override(ContainerRegistry) - def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry": - return cast(CuraContainerRegistry, super().getInstance(*args, **kwargs)) +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import os +import re +import configparser + +from typing import Any, cast, Dict, Optional, List, Union +from PyQt5.QtWidgets import QMessageBox + +from UM.Decorators import override +from UM.Settings.ContainerFormatError import ContainerFormatError +from UM.Settings.Interfaces import ContainerInterface +from UM.Settings.ContainerRegistry import ContainerRegistry +from UM.Settings.ContainerStack import ContainerStack +from UM.Settings.InstanceContainer import InstanceContainer +from UM.Settings.SettingInstance import SettingInstance +from UM.Logger import Logger +from UM.Message import Message +from UM.Platform import Platform +from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with. +from UM.Resources import Resources +from UM.Util import parseBool +from cura.ReaderWriters.ProfileWriter import ProfileWriter + +from . import ExtruderStack +from . import GlobalStack + +import cura.CuraApplication +from cura.Settings.cura_empty_instance_containers import empty_quality_container +from cura.Machines.ContainerTree import ContainerTree +from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader + +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") + + +class CuraContainerRegistry(ContainerRegistry): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack + # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack + # is added, we check to see if an extruder stack needs to be added. + self.containerAdded.connect(self._onContainerAdded) + + @override(ContainerRegistry) + def addContainer(self, container: ContainerInterface) -> None: + """Overridden from ContainerRegistry + + Adds a container to the registry. + + This will also try to convert a ContainerStack to either Extruder or + Global stack based on metadata information. + """ + + # Note: Intentional check with type() because we want to ignore subclasses + if type(container) == ContainerStack: + container = self._convertContainerStack(cast(ContainerStack, container)) + + if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()): + # Check against setting version of the definition. + required_setting_version = cura.CuraApplication.CuraApplication.SettingVersion + actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0)) + if required_setting_version != actual_setting_version: + Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version)) + return # Don't add. + + super().addContainer(container) + + def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str: + """Create a name that is not empty and unique + + :param container_type: :type{string} Type of the container (machine, quality, ...) + :param current_name: :type{} Current name of the container, which may be an acceptable option + :param new_name: :type{string} Base name, which may not be unique + :param fallback_name: :type{string} Name to use when (stripped) new_name is empty + :return: :type{string} Name that is unique for the specified type and name/id + """ + new_name = new_name.strip() + num_check = re.compile(r"(.*?)\s*#\d+$").match(new_name) + if num_check: + new_name = num_check.group(1) + if new_name == "": + new_name = fallback_name + + unique_name = new_name + i = 1 + # In case we are renaming, the current name of the container is also a valid end-result + while self._containerExists(container_type, unique_name) and unique_name != current_name: + i += 1 + unique_name = "%s #%d" % (new_name, i) + + return unique_name + + def _containerExists(self, container_type: str, container_name: str): + """Check if a container with of a certain type and a certain name or id exists + + Both the id and the name are checked, because they may not be the same and it is better if they are both unique + :param container_type: :type{string} Type of the container (machine, quality, ...) + :param container_name: :type{string} Name to check + """ + container_class = ContainerStack if container_type == "machine" else InstanceContainer + + return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \ + self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type) + + def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool: + """Exports an profile to a file + + :param container_list: :type{list} the containers to export. This is not + necessarily in any order! + :param file_name: :type{str} the full path and filename to export to. + :param file_type: :type{str} the file type with the format " (*.)" + :return: True if the export succeeded, false otherwise. + """ + + # Parse the fileType to deduce what plugin can save the file format. + # fileType has the format " (*.)" + split = file_type.rfind(" (*.") # Find where the description ends and the extension starts. + if split < 0: # Not found. Invalid format. + Logger.log("e", "Invalid file format identifier %s", file_type) + return False + description = file_type[:split] + extension = file_type[split + 4:-1] # Leave out the " (*." and ")". + if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any. + file_name += "." + extension + + # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself. + if not Platform.isWindows(): + if os.path.exists(file_name): + result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"), + catalog.i18nc("@label Don't translate the XML tag !", "The file {0} already exists. Are you sure you want to overwrite it?").format(file_name)) + if result == QMessageBox.No: + return False + + profile_writer = self._findProfileWriter(extension, description) + try: + if profile_writer is None: + raise Exception("Unable to find a profile writer") + success = profile_writer.write(file_name, container_list) + except Exception as e: + Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e)) + m = Message(catalog.i18nc("@info:status Don't translate the XML tags or !", "Failed to export profile to {0}: {1}", file_name, str(e)), + lifetime = 0, + title = catalog.i18nc("@info:title", "Error")) + m.show() + return False + if not success: + Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name) + m = Message(catalog.i18nc("@info:status Don't translate the XML tag !", "Failed to export profile to {0}: Writer plugin reported failure.", file_name), + lifetime = 0, + title = catalog.i18nc("@info:title", "Error")) + m.show() + return False + m = Message(catalog.i18nc("@info:status Don't translate the XML tag !", "Exported profile to {0}", file_name), + title = catalog.i18nc("@info:title", "Export succeeded")) + m.show() + return True + + def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]: + """Gets the plugin object matching the criteria + + :param extension: + :param description: + :return: The plugin object matching the given extension and description. + """ + plugin_registry = PluginRegistry.getInstance() + for plugin_id, meta_data in self._getIOPlugins("profile_writer"): + for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write. + supported_extension = supported_type.get("extension", None) + if supported_extension == extension: # This plugin supports a file type with the same extension. + supported_description = supported_type.get("description", None) + if supported_description == description: # The description is also identical. Assume it's the same file type. + return cast(ProfileWriter, plugin_registry.getPluginObject(plugin_id)) + return None + + def importProfile(self, file_name: str) -> Dict[str, str]: + """Imports a profile from a file + + :param file_name: The full path and filename of the profile to import. + :return: Dict with a 'status' key containing the string 'ok' or 'error', + and a 'message' key containing a message for the user. + """ + + Logger.log("d", "Attempting to import profile %s", file_name) + if not file_name: + return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Failed to import profile from {0}: {1}", file_name, "Invalid path")} + + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() + if not global_stack: + return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Can't import profile from {0} before a printer is added.", file_name)} + container_tree = ContainerTree.getInstance() + + machine_extruders = global_stack.extruderList + + plugin_registry = PluginRegistry.getInstance() + extension = file_name.split(".")[-1] + + for plugin_id, meta_data in self._getIOPlugins("profile_reader"): + if meta_data["profile_reader"][0]["extension"] != extension: + continue + profile_reader = cast(ProfileReader, plugin_registry.getPluginObject(plugin_id)) + try: + profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader. + except NoProfileException: + return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "No custom profile to import in file {0}", file_name)} + except Exception as e: + # Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None. + Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e)) + return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Failed to import profile from {0}:", file_name) + "\n" + str(e) + ""} + + if profile_or_list: + # Ensure it is always a list of profiles + if not isinstance(profile_or_list, list): + profile_or_list = [profile_or_list] + + # First check if this profile is suitable for this machine + global_profile = None + extruder_profiles = [] + if len(profile_or_list) == 1: + global_profile = profile_or_list[0] + else: + for profile in profile_or_list: + if not profile.getMetaDataEntry("position"): + global_profile = profile + else: + extruder_profiles.append(profile) + extruder_profiles = sorted(extruder_profiles, key = lambda x: int(x.getMetaDataEntry("position"))) + profile_or_list = [global_profile] + extruder_profiles + + if not global_profile: + Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name) + return { "status": "error", + "message": catalog.i18nc("@info:status Don't translate the XML tags !", "This profile {0} contains incorrect data, could not import it.", file_name)} + profile_definition = global_profile.getMetaDataEntry("definition") + + # Make sure we have a profile_definition in the file: + if profile_definition is None: + break + machine_definitions = self.findContainers(id = profile_definition) + if not machine_definitions: + Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition) + return {"status": "error", + "message": catalog.i18nc("@info:status Don't translate the XML tags !", "This profile {0} contains incorrect data, could not import it.", file_name) + } + machine_definition = machine_definitions[0] + + # Get the expected machine definition. + # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... + has_machine_quality = parseBool(machine_definition.getMetaDataEntry("has_machine_quality", "false")) + profile_definition = machine_definition.getMetaDataEntry("quality_definition", machine_definition.getId()) if has_machine_quality else "fdmprinter" + expected_machine_definition = container_tree.machines[global_stack.definition.getId()].quality_definition + + # And check if the profile_definition matches either one (showing error if not): + if profile_definition != expected_machine_definition: + Logger.log("d", "Profile {file_name} is for machine {profile_definition}, but the current active machine is {expected_machine_definition}. Changing profile's definition.".format(file_name = file_name, profile_definition = profile_definition, expected_machine_definition = expected_machine_definition)) + global_profile.setMetaDataEntry("definition", expected_machine_definition) + for extruder_profile in extruder_profiles: + extruder_profile.setMetaDataEntry("definition", expected_machine_definition) + + quality_name = global_profile.getName() + quality_type = global_profile.getMetaDataEntry("quality_type") + + name_seed = os.path.splitext(os.path.basename(file_name))[0] + new_name = self.uniqueName(name_seed) + + # Ensure it is always a list of profiles + if type(profile_or_list) is not list: + profile_or_list = [profile_or_list] + + # Make sure that there are also extruder stacks' quality_changes, not just one for the global stack + if len(profile_or_list) == 1: + global_profile = profile_or_list[0] + extruder_profiles = [] + for idx, extruder in enumerate(global_stack.extruderList): + profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1)) + profile = InstanceContainer(profile_id) + profile.setName(quality_name) + profile.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion) + profile.setMetaDataEntry("type", "quality_changes") + profile.setMetaDataEntry("definition", expected_machine_definition) + profile.setMetaDataEntry("quality_type", quality_type) + profile.setDirty(True) + if idx == 0: + # Move all per-extruder settings to the first extruder's quality_changes + for qc_setting_key in global_profile.getAllKeys(): + settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = global_profile.getProperty(qc_setting_key, "value") + + setting_definition = global_stack.getSettingDefinition(qc_setting_key) + if setting_definition is not None: + new_instance = SettingInstance(setting_definition, profile) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + profile.addInstance(new_instance) + profile.setDirty(True) + + global_profile.removeInstance(qc_setting_key, postpone_emit = True) + extruder_profiles.append(profile) + + for profile in extruder_profiles: + profile_or_list.append(profile) + + # Import all profiles + profile_ids_added = [] # type: List[str] + for profile_index, profile in enumerate(profile_or_list): + if profile_index == 0: + # This is assumed to be the global profile + profile_id = (cast(ContainerInterface, global_stack.getBottom()).getId() + "_" + name_seed).lower().replace(" ", "_") + + elif profile_index < len(machine_extruders) + 1: + # This is assumed to be an extruder profile + extruder_id = machine_extruders[profile_index - 1].definition.getId() + extruder_position = str(profile_index - 1) + if not profile.getMetaDataEntry("position"): + profile.setMetaDataEntry("position", extruder_position) + else: + profile.setMetaDataEntry("position", extruder_position) + profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_") + + else: # More extruders in the imported file than in the machine. + continue # Delete the additional profiles. + + result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition) + if result is not None: + # Remove any profiles that did got added. + for profile_id in profile_ids_added: + self.removeContainer(profile_id) + + return {"status": "error", "message": catalog.i18nc( + "@info:status Don't translate the XML tag !", + "Failed to import profile from {0}:", + file_name) + " " + result} + profile_ids_added.append(profile.getId()) + return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} + + # This message is throw when the profile reader doesn't find any profile in the file + return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)} + + # If it hasn't returned by now, none of the plugins loaded the profile successfully. + return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)} + + @override(ContainerRegistry) + def load(self) -> None: + super().load() + self._registerSingleExtrusionMachinesExtruderStacks() + self._connectUpgradedExtruderStacksToMachines() + + @override(ContainerRegistry) + def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool: + """Check if the metadata for a container is okay before adding it. + + This overrides the one from UM.Settings.ContainerRegistry because we + also require that the setting_version is correct. + """ + + if metadata is None: + return False + if "setting_version" not in metadata: + return False + try: + if int(metadata["setting_version"]) != cura.CuraApplication.CuraApplication.SettingVersion: + return False + except ValueError: #Not parsable as int. + return False + return True + + def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]: + """Update an imported profile to match the current machine configuration. + + :param profile: The profile to configure. + :param id_seed: The base ID for the profile. May be changed so it does not conflict with existing containers. + :param new_name: The new name for the profile. + + :return: None if configuring was successful or an error message if an error occurred. + """ + + profile.setDirty(True) # Ensure the profiles are correctly saved + + new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile")) + profile.setMetaDataEntry("id", new_id) + profile.setName(new_name) + + # Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile + # It also solves an issue with importing profiles from G-Codes + profile.setMetaDataEntry("id", new_id) + profile.setMetaDataEntry("definition", machine_definition_id) + + if "type" in profile.getMetaData(): + profile.setMetaDataEntry("type", "quality_changes") + else: + profile.setMetaDataEntry("type", "quality_changes") + + quality_type = profile.getMetaDataEntry("quality_type") + if not quality_type: + return catalog.i18nc("@info:status", "Profile is missing a quality type.") + + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() + if global_stack is None: + return None + definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition + profile.setDefinition(definition_id) + + # Check to make sure the imported profile actually makes sense in context of the current configuration. + # This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as + # successfully imported but then fail to show up. + quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups() + # "not_supported" profiles can be imported. + if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict: + return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type) + + ContainerRegistry.getInstance().addContainer(profile) + + return None + + @override(ContainerRegistry) + def saveDirtyContainers(self) -> None: + # Lock file for "more" atomically loading and saving to/from config dir. + with self.lockFile(): + # Save base files first + for instance in self.findDirtyContainers(container_type=InstanceContainer): + if instance.getMetaDataEntry("removed"): + continue + if instance.getId() == instance.getMetaData().get("base_file"): + self.saveContainer(instance) + + for instance in self.findDirtyContainers(container_type=InstanceContainer): + if instance.getMetaDataEntry("removed"): + continue + self.saveContainer(instance) + + for stack in self.findContainerStacks(): + self.saveContainer(stack) + + def _getIOPlugins(self, io_type): + """Gets a list of profile writer plugins + + :return: List of tuples of (plugin_id, meta_data). + """ + plugin_registry = PluginRegistry.getInstance() + active_plugin_ids = plugin_registry.getActivePlugins() + + result = [] + for plugin_id in active_plugin_ids: + meta_data = plugin_registry.getMetaData(plugin_id) + if io_type in meta_data: + result.append( (plugin_id, meta_data) ) + return result + + def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]: + """Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.""" + + assert type(container) == ContainerStack + + container_type = container.getMetaDataEntry("type") + if container_type not in ("extruder_train", "machine"): + # It is not an extruder or machine, so do nothing with the stack + return container + + Logger.log("d", "Converting ContainerStack {stack} to {type}", stack = container.getId(), type = container_type) + + if container_type == "extruder_train": + new_stack = ExtruderStack.ExtruderStack(container.getId()) + else: + new_stack = GlobalStack.GlobalStack(container.getId()) + + container_contents = container.serialize() + new_stack.deserialize(container_contents) + + # Delete the old configuration file so we do not get double stacks + if os.path.isfile(container.getPath()): + os.remove(container.getPath()) + + return new_stack + + def _registerSingleExtrusionMachinesExtruderStacks(self) -> None: + machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"}) + for machine in machines: + extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId()) + if not extruder_stacks: + self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder") + + def _onContainerAdded(self, container: ContainerInterface) -> None: + # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack + # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack + # is added, we check to see if an extruder stack needs to be added. + if not isinstance(container, ContainerStack) or container.getMetaDataEntry("type") != "machine": + return + + machine_extruder_trains = container.getMetaDataEntry("machine_extruder_trains") + if machine_extruder_trains is not None and machine_extruder_trains != {"0": "fdmextruder"}: + return + + extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = container.getId()) + if not extruder_stacks: + self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder") + + # + # new_global_quality_changes is optional. It is only used in project loading for a scenario like this: + # - override the current machine + # - create new for custom quality profile + # new_global_quality_changes is the new global quality changes container in this scenario. + # create_new_ids indicates if new unique ids must be created + # + def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None, create_new_ids = True): + new_extruder_id = extruder_id + + application = cura.CuraApplication.CuraApplication.getInstance() + + extruder_definitions = self.findDefinitionContainers(id = new_extruder_id) + if not extruder_definitions: + Logger.log("w", "Could not find definition containers for extruder %s", new_extruder_id) + return + + extruder_definition = extruder_definitions[0] + unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) if create_new_ids else machine.getName() + " " + new_extruder_id + + extruder_stack = ExtruderStack.ExtruderStack(unique_name) + extruder_stack.setName(extruder_definition.getName()) + extruder_stack.setDefinition(extruder_definition) + extruder_stack.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) + + # create a new definition_changes container for the extruder stack + definition_changes_id = self.uniqueName(extruder_stack.getId() + "_settings") if create_new_ids else extruder_stack.getId() + "_settings" + definition_changes_name = definition_changes_id + definition_changes = InstanceContainer(definition_changes_id, parent = application) + definition_changes.setName(definition_changes_name) + definition_changes.setMetaDataEntry("setting_version", application.SettingVersion) + definition_changes.setMetaDataEntry("type", "definition_changes") + definition_changes.setMetaDataEntry("definition", extruder_definition.getId()) + + # move definition_changes settings if exist + for setting_key in definition_changes.getAllKeys(): + if machine.definition.getProperty(setting_key, "settable_per_extruder"): + setting_value = machine.definitionChanges.getProperty(setting_key, "value") + if setting_value is not None: + # move it to the extruder stack's definition_changes + setting_definition = machine.getSettingDefinition(setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + definition_changes.addInstance(new_instance) + definition_changes.setDirty(True) + + machine.definitionChanges.removeInstance(setting_key, postpone_emit = True) + + self.addContainer(definition_changes) + extruder_stack.setDefinitionChanges(definition_changes) + + # create empty user changes container otherwise + user_container_id = self.uniqueName(extruder_stack.getId() + "_user") if create_new_ids else extruder_stack.getId() + "_user" + user_container_name = user_container_id + user_container = InstanceContainer(user_container_id, parent = application) + user_container.setName(user_container_name) + user_container.setMetaDataEntry("type", "user") + user_container.setMetaDataEntry("machine", machine.getId()) + user_container.setMetaDataEntry("setting_version", application.SettingVersion) + user_container.setDefinition(machine.definition.getId()) + user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position")) + + if machine.userChanges: + # For the newly created extruder stack, we need to move all "per-extruder" settings to the user changes + # container to the extruder stack. + for user_setting_key in machine.userChanges.getAllKeys(): + settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = machine.getProperty(user_setting_key, "value") + + setting_definition = machine.getSettingDefinition(user_setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + user_container.addInstance(new_instance) + user_container.setDirty(True) + + machine.userChanges.removeInstance(user_setting_key, postpone_emit = True) + + self.addContainer(user_container) + extruder_stack.setUserChanges(user_container) + + empty_variant = application.empty_variant_container + empty_material = application.empty_material_container + empty_quality = application.empty_quality_container + + if machine.variant.getId() not in ("empty", "empty_variant"): + variant = machine.variant + else: + variant = empty_variant + extruder_stack.variant = variant + + if machine.material.getId() not in ("empty", "empty_material"): + material = machine.material + else: + material = empty_material + extruder_stack.material = material + + if machine.quality.getId() not in ("empty", "empty_quality"): + quality = machine.quality + else: + quality = empty_quality + extruder_stack.quality = quality + + machine_quality_changes = machine.qualityChanges + if new_global_quality_changes is not None: + machine_quality_changes = new_global_quality_changes + + if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): + extruder_quality_changes_container = self.findInstanceContainers(name = machine_quality_changes.getName(), extruder = extruder_id) + if extruder_quality_changes_container: + extruder_quality_changes_container = extruder_quality_changes_container[0] + + quality_changes_id = extruder_quality_changes_container.getId() + extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] + else: + # Some extruder quality_changes containers can be created at runtime as files in the qualities + # folder. Those files won't be loaded in the registry immediately. So we also need to search + # the folder to see if the quality_changes exists. + extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) + if extruder_quality_changes_container: + quality_changes_id = extruder_quality_changes_container.getId() + extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) + extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] + else: + # If we still cannot find a quality changes container for the extruder, create a new one + container_name = machine_quality_changes.getName() + container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name) + extruder_quality_changes_container = InstanceContainer(container_id, parent = application) + extruder_quality_changes_container.setName(container_name) + extruder_quality_changes_container.setMetaDataEntry("type", "quality_changes") + extruder_quality_changes_container.setMetaDataEntry("setting_version", application.SettingVersion) + extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) + extruder_quality_changes_container.setMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type")) + extruder_quality_changes_container.setMetaDataEntry("intent_category", "default") # Intent categories weren't a thing back then. + extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId()) + + self.addContainer(extruder_quality_changes_container) + extruder_stack.qualityChanges = extruder_quality_changes_container + + if not extruder_quality_changes_container: + Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", + machine_quality_changes.getName(), extruder_stack.getId()) + else: + # Move all per-extruder settings to the extruder's quality changes + for qc_setting_key in machine_quality_changes.getAllKeys(): + settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = machine_quality_changes.getProperty(qc_setting_key, "value") + + setting_definition = machine.getSettingDefinition(qc_setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + extruder_quality_changes_container.addInstance(new_instance) + extruder_quality_changes_container.setDirty(True) + + machine_quality_changes.removeInstance(qc_setting_key, postpone_emit=True) + else: + extruder_stack.qualityChanges = self.findInstanceContainers(id = "empty_quality_changes")[0] + + self.addContainer(extruder_stack) + + # Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have + # per-extruder settings in the container for the machine instead of the extruder. + if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): + quality_changes_machine_definition_id = machine_quality_changes.getDefinition().getId() + else: + whole_machine_definition = machine.definition + machine_entry = machine.definition.getMetaDataEntry("machine") + if machine_entry is not None: + container_registry = ContainerRegistry.getInstance() + whole_machine_definition = container_registry.findDefinitionContainers(id = machine_entry)[0] + + quality_changes_machine_definition_id = "fdmprinter" + if whole_machine_definition.getMetaDataEntry("has_machine_quality"): + quality_changes_machine_definition_id = machine.definition.getMetaDataEntry("quality_definition", + whole_machine_definition.getId()) + qcs = self.findInstanceContainers(type = "quality_changes", definition = quality_changes_machine_definition_id) + qc_groups = {} # map of qc names -> qc containers + for qc in qcs: + qc_name = qc.getName() + if qc_name not in qc_groups: + qc_groups[qc_name] = [] + qc_groups[qc_name].append(qc) + # Try to find from the quality changes cura directory too + quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) + if quality_changes_container: + qc_groups[qc_name].append(quality_changes_container) + + for qc_name, qc_list in qc_groups.items(): + qc_dict = {"global": None, "extruders": []} + for qc in qc_list: + extruder_position = qc.getMetaDataEntry("position") + if extruder_position is not None: + qc_dict["extruders"].append(qc) + else: + qc_dict["global"] = qc + if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1: + # Move per-extruder settings + for qc_setting_key in qc_dict["global"].getAllKeys(): + settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = qc_dict["global"].getProperty(qc_setting_key, "value") + + setting_definition = machine.getSettingDefinition(qc_setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + qc_dict["extruders"][0].addInstance(new_instance) + qc_dict["extruders"][0].setDirty(True) + + qc_dict["global"].removeInstance(qc_setting_key, postpone_emit=True) + + # Set next stack at the end + extruder_stack.setNextStack(machine) + + return extruder_stack + + def _findQualityChangesContainerInCuraFolder(self, name: str) -> Optional[InstanceContainer]: + quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer) + + instance_container = None + + for item in os.listdir(quality_changes_dir): + file_path = os.path.join(quality_changes_dir, item) + if not os.path.isfile(file_path): + continue + + parser = configparser.ConfigParser(interpolation = None) + try: + parser.read([file_path]) + except Exception: + # Skip, it is not a valid stack file + continue + + if not parser.has_option("general", "name"): + continue + + if parser["general"]["name"] == name: + # Load the container + container_id = os.path.basename(file_path).replace(".inst.cfg", "") + if self.findInstanceContainers(id = container_id): + # This container is already in the registry, skip it + continue + + instance_container = InstanceContainer(container_id) + with open(file_path, "r", encoding = "utf-8") as f: + serialized = f.read() + try: + instance_container.deserialize(serialized, file_path) + except ContainerFormatError: + Logger.logException("e", "Unable to deserialize InstanceContainer %s", file_path) + continue + self.addContainer(instance_container) + break + + return instance_container + + # Fix the extruders that were upgraded to ExtruderStack instances during addContainer. + # The stacks are now responsible for setting the next stack on deserialize. However, + # due to problems with loading order, some stacks may not have the proper next stack + # set after upgrading, because the proper global stack was not yet loaded. This method + # makes sure those extruders also get the right stack set. + def _connectUpgradedExtruderStacksToMachines(self) -> None: + extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack) + for extruder_stack in extruder_stacks: + if extruder_stack.getNextStack(): + # Has the right next stack, so ignore it. + continue + + machines = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack.getMetaDataEntry("machine", "")) + if machines: + extruder_stack.setNextStack(machines[0]) + else: + Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId()) + + # Override just for the type. + @classmethod + @override(ContainerRegistry) + def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry": + return cast(CuraContainerRegistry, super().getInstance(*args, **kwargs)) diff --git a/cura/Settings/CuraContainerStack.py b/cura/Settings/CuraContainerStack.py index 1455e140a8..4595bf3996 100755 --- a/cura/Settings/CuraContainerStack.py +++ b/cura/Settings/CuraContainerStack.py @@ -18,25 +18,27 @@ from cura.Settings import cura_empty_instance_containers from . import Exceptions -## Base class for Cura related stacks that want to enforce certain containers are available. -# -# This class makes sure that the stack has the following containers set: user changes, quality -# changes, quality, material, variant, definition changes and finally definition. Initially, -# these will be equal to the empty instance container. -# -# The container types are determined based on the following criteria: -# - user: An InstanceContainer with the metadata entry "type" set to "user". -# - quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes". -# - quality: An InstanceContainer with the metadata entry "type" set to "quality". -# - material: An InstanceContainer with the metadata entry "type" set to "material". -# - variant: An InstanceContainer with the metadata entry "type" set to "variant". -# - definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes". -# - definition: A DefinitionContainer. -# -# Internally, this class ensures the mentioned containers are always there and kept in a specific order. -# This also means that operations on the stack that modifies the container ordering is prohibited and -# will raise an exception. class CuraContainerStack(ContainerStack): + """Base class for Cura related stacks that want to enforce certain containers are available. + + This class makes sure that the stack has the following containers set: user changes, quality + changes, quality, material, variant, definition changes and finally definition. Initially, + these will be equal to the empty instance container. + + The container types are determined based on the following criteria: + - user: An InstanceContainer with the metadata entry "type" set to "user". + - quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes". + - quality: An InstanceContainer with the metadata entry "type" set to "quality". + - material: An InstanceContainer with the metadata entry "type" set to "material". + - variant: An InstanceContainer with the metadata entry "type" set to "variant". + - definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes". + - definition: A DefinitionContainer. + + Internally, this class ensures the mentioned containers are always there and kept in a specific order. + This also means that operations on the stack that modifies the container ordering is prohibited and + will raise an exception. + """ + def __init__(self, container_id: str) -> None: super().__init__(container_id) @@ -58,104 +60,136 @@ class CuraContainerStack(ContainerStack): import cura.CuraApplication #Here to prevent circular imports. self.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion) + self.setDirty(False) + # This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted. pyqtContainersChanged = pyqtSignal() - ## Set the user changes container. - # - # \param new_user_changes The new user changes container. It is expected to have a "type" metadata entry with the value "user". def setUserChanges(self, new_user_changes: InstanceContainer) -> None: + """Set the user changes container. + + :param new_user_changes: The new user changes container. It is expected to have a "type" metadata entry with the value "user". + """ + self.replaceContainer(_ContainerIndexes.UserChanges, new_user_changes) - ## Get the user changes container. - # - # \return The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setUserChanges, notify = pyqtContainersChanged) def userChanges(self) -> InstanceContainer: + """Get the user changes container. + + :return: The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.UserChanges]) - ## Set the quality changes container. - # - # \param new_quality_changes The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes". def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None: + """Set the quality changes container. + + :param new_quality_changes: The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes". + """ + self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes, postpone_emit = postpone_emit) - ## Get the quality changes container. - # - # \return The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setQualityChanges, notify = pyqtContainersChanged) def qualityChanges(self) -> InstanceContainer: + """Get the quality changes container. + + :return: The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.QualityChanges]) - ## Set the intent container. - # - # \param new_intent The new intent container. It is expected to have a "type" metadata entry with the value "intent". def setIntent(self, new_intent: InstanceContainer, postpone_emit: bool = False) -> None: + """Set the intent container. + + :param new_intent: The new intent container. It is expected to have a "type" metadata entry with the value "intent". + """ + self.replaceContainer(_ContainerIndexes.Intent, new_intent, postpone_emit = postpone_emit) - ## Get the quality container. - # - # \return The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setIntent, notify = pyqtContainersChanged) def intent(self) -> InstanceContainer: + """Get the quality container. + + :return: The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.Intent]) - ## Set the quality container. - # - # \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality". def setQuality(self, new_quality: InstanceContainer, postpone_emit: bool = False) -> None: + """Set the quality container. + + :param new_quality: The new quality container. It is expected to have a "type" metadata entry with the value "quality". + """ + self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit) - ## Get the quality container. - # - # \return The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setQuality, notify = pyqtContainersChanged) def quality(self) -> InstanceContainer: + """Get the quality container. + + :return: The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.Quality]) - ## Set the material container. - # - # \param new_material The new material container. It is expected to have a "type" metadata entry with the value "material". def setMaterial(self, new_material: InstanceContainer, postpone_emit: bool = False) -> None: + """Set the material container. + + :param new_material: The new material container. It is expected to have a "type" metadata entry with the value "material". + """ + self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit) - ## Get the material container. - # - # \return The material container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setMaterial, notify = pyqtContainersChanged) def material(self) -> InstanceContainer: + """Get the material container. + + :return: The material container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.Material]) - ## Set the variant container. - # - # \param new_variant The new variant container. It is expected to have a "type" metadata entry with the value "variant". def setVariant(self, new_variant: InstanceContainer) -> None: + """Set the variant container. + + :param new_variant: The new variant container. It is expected to have a "type" metadata entry with the value "variant". + """ + self.replaceContainer(_ContainerIndexes.Variant, new_variant) - ## Get the variant container. - # - # \return The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setVariant, notify = pyqtContainersChanged) def variant(self) -> InstanceContainer: + """Get the variant container. + + :return: The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.Variant]) - ## Set the definition changes container. - # - # \param new_definition_changes The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes". def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None: + """Set the definition changes container. + + :param new_definition_changes: The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes". + """ + self.replaceContainer(_ContainerIndexes.DefinitionChanges, new_definition_changes) - ## Get the definition changes container. - # - # \return The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setDefinitionChanges, notify = pyqtContainersChanged) def definitionChanges(self) -> InstanceContainer: + """Get the definition changes container. + + :return: The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.DefinitionChanges]) - ## Set the definition container. - # - # \param new_definition The new definition container. It is expected to have a "type" metadata entry with the value "definition". def setDefinition(self, new_definition: DefinitionContainerInterface) -> None: + """Set the definition container. + + :param new_definition: The new definition container. It is expected to have a "type" metadata entry with the value "definition". + """ + self.replaceContainer(_ContainerIndexes.Definition, new_definition) def getDefinition(self) -> "DefinitionContainer": @@ -171,14 +205,16 @@ class CuraContainerStack(ContainerStack): def getTop(self) -> "InstanceContainer": return self.userChanges - ## Check whether the specified setting has a 'user' value. - # - # A user value here is defined as the setting having a value in either - # the UserChanges or QualityChanges container. - # - # \return True if the setting has a user value, False if not. @pyqtSlot(str, result = bool) def hasUserValue(self, key: str) -> bool: + """Check whether the specified setting has a 'user' value. + + A user value here is defined as the setting having a value in either + the UserChanges or QualityChanges container. + + :return: True if the setting has a user value, False if not. + """ + if self._containers[_ContainerIndexes.UserChanges].hasProperty(key, "value"): return True @@ -187,51 +223,61 @@ class CuraContainerStack(ContainerStack): return False - ## Set a property of a setting. - # - # This will set a property of a specified setting. Since the container stack does not contain - # any settings itself, it is required to specify a container to set the property on. The target - # container is matched by container type. - # - # \param key The key of the setting to set. - # \param property_name The name of the property to set. - # \param new_value The new value to set the property to. def setProperty(self, key: str, property_name: str, property_value: Any, container: "ContainerInterface" = None, set_from_cache: bool = False) -> None: + """Set a property of a setting. + + This will set a property of a specified setting. Since the container stack does not contain + any settings itself, it is required to specify a container to set the property on. The target + container is matched by container type. + + :param key: The key of the setting to set. + :param property_name: The name of the property to set. + :param new_value: The new value to set the property to. + """ + container_index = _ContainerIndexes.UserChanges self._containers[container_index].setProperty(key, property_name, property_value, container, set_from_cache) - ## Overridden from ContainerStack - # - # Since we have a fixed order of containers in the stack and this method would modify the container - # ordering, we disallow this operation. @override(ContainerStack) def addContainer(self, container: ContainerInterface) -> None: + """Overridden from ContainerStack + + Since we have a fixed order of containers in the stack and this method would modify the container + ordering, we disallow this operation. + """ + raise Exceptions.InvalidOperationError("Cannot add a container to Global stack") - ## Overridden from ContainerStack - # - # Since we have a fixed order of containers in the stack and this method would modify the container - # ordering, we disallow this operation. @override(ContainerStack) def insertContainer(self, index: int, container: ContainerInterface) -> None: + """Overridden from ContainerStack + + Since we have a fixed order of containers in the stack and this method would modify the container + ordering, we disallow this operation. + """ + raise Exceptions.InvalidOperationError("Cannot insert a container into Global stack") - ## Overridden from ContainerStack - # - # Since we have a fixed order of containers in the stack and this method would modify the container - # ordering, we disallow this operation. @override(ContainerStack) def removeContainer(self, index: int = 0) -> None: + """Overridden from ContainerStack + + Since we have a fixed order of containers in the stack and this method would modify the container + ordering, we disallow this operation. + """ + raise Exceptions.InvalidOperationError("Cannot remove a container from Global stack") - ## Overridden from ContainerStack - # - # Replaces the container at the specified index with another container. - # This version performs checks to make sure the new container has the expected metadata and type. - # - # \throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type. @override(ContainerStack) def replaceContainer(self, index: int, container: ContainerInterface, postpone_emit: bool = False) -> None: + """Overridden from ContainerStack + + Replaces the container at the specified index with another container. + This version performs checks to make sure the new container has the expected metadata and type. + + :throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type. + """ + expected_type = _ContainerIndexes.IndexTypeMap[index] if expected_type == "definition": if not isinstance(container, DefinitionContainer): @@ -245,16 +291,18 @@ class CuraContainerStack(ContainerStack): super().replaceContainer(index, container, postpone_emit) - ## Overridden from ContainerStack - # - # This deserialize will make sure the internal list of containers matches with what we expect. - # It will first check to see if the container at a certain index already matches with what we - # expect. If it does not, it will search for a matching container with the correct type. Should - # no container with the correct type be found, it will use the empty container. - # - # \throws InvalidContainerStackError Raised when no definition can be found for the stack. @override(ContainerStack) def deserialize(self, serialized: str, file_name: Optional[str] = None) -> str: + """Overridden from ContainerStack + + This deserialize will make sure the internal list of containers matches with what we expect. + It will first check to see if the container at a certain index already matches with what we + expect. If it does not, it will search for a matching container with the correct type. Should + no container with the correct type be found, it will use the empty container. + + :raise InvalidContainerStackError: Raised when no definition can be found for the stack. + """ + # update the serialized data first serialized = super().deserialize(serialized, file_name) @@ -298,10 +346,9 @@ class CuraContainerStack(ContainerStack): ## TODO; Deserialize the containers. return serialized - ## protected: - - # Helper to make sure we emit a PyQt signal on container changes. def _onContainersChanged(self, container: Any) -> None: + """Helper to make sure we emit a PyQt signal on container changes.""" + Application.getInstance().callLater(self.pyqtContainersChanged.emit) # Helper that can be overridden to get the "machine" definition, that is, the definition that defines the machine @@ -309,16 +356,18 @@ class CuraContainerStack(ContainerStack): def _getMachineDefinition(self) -> DefinitionContainer: return self.definition - ## Find the ID that should be used when searching for instance containers for a specified definition. - # - # This handles the situation where the definition specifies we should use a different definition when - # searching for instance containers. - # - # \param machine_definition The definition to find the "quality definition" for. - # - # \return The ID of the definition container to use when searching for instance containers. @classmethod def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainerInterface) -> str: + """Find the ID that should be used when searching for instance containers for a specified definition. + + This handles the situation where the definition specifies we should use a different definition when + searching for instance containers. + + :param machine_definition: The definition to find the "quality definition" for. + + :return: The ID of the definition container to use when searching for instance containers. + """ + quality_definition = machine_definition.getMetaDataEntry("quality_definition") if not quality_definition: return machine_definition.id #type: ignore @@ -330,17 +379,18 @@ class CuraContainerStack(ContainerStack): return cls._findInstanceContainerDefinitionId(definitions[0]) - ## getProperty for extruder positions, with translation from -1 to default extruder number def getExtruderPositionValueWithDefault(self, key): + """getProperty for extruder positions, with translation from -1 to default extruder number""" + value = self.getProperty(key, "value") if value == -1: value = int(Application.getInstance().getMachineManager().defaultExtruderPosition) return value -## private: -# Private helper class to keep track of container positions and their types. class _ContainerIndexes: + """Private helper class to keep track of container positions and their types.""" + UserChanges = 0 QualityChanges = 1 Intent = 2 diff --git a/cura/Settings/CuraStackBuilder.py b/cura/Settings/CuraStackBuilder.py index 257af78ecc..2b71db8034 100644 --- a/cura/Settings/CuraStackBuilder.py +++ b/cura/Settings/CuraStackBuilder.py @@ -13,17 +13,20 @@ from .GlobalStack import GlobalStack from .ExtruderStack import ExtruderStack -## Contains helper functions to create new machines. class CuraStackBuilder: + """Contains helper functions to create new machines.""" + - ## Create a new instance of a machine. - # - # \param name The name of the new machine. - # \param definition_id The ID of the machine definition to use. - # - # \return The new global stack or None if an error occurred. @classmethod def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]: + """Create a new instance of a machine. + + :param name: The name of the new machine. + :param definition_id: The ID of the machine definition to use. + + :return: The new global stack or None if an error occurred. + """ + from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() registry = application.getContainerRegistry() @@ -62,7 +65,7 @@ class CuraStackBuilder: except IndexError: return None - for new_extruder in new_global_stack.extruders.values(): # Only register the extruders if we're sure that all of them are correct. + for new_extruder in new_global_stack.extruderList: # Only register the extruders if we're sure that all of them are correct. registry.addContainer(new_extruder) # Register the global stack after the extruder stacks are created. This prevents the registry from adding another @@ -71,12 +74,14 @@ class CuraStackBuilder: return new_global_stack - ## Create a default Extruder Stack - # - # \param global_stack The global stack this extruder refers to. - # \param extruder_position The position of the current extruder. @classmethod def createExtruderStackWithDefaultSetup(cls, global_stack: "GlobalStack", extruder_position: int) -> None: + """Create a default Extruder Stack + + :param global_stack: The global stack this extruder refers to. + :param extruder_position: The position of the current extruder. + """ + from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() registry = application.getContainerRegistry() @@ -120,17 +125,6 @@ class CuraStackBuilder: registry.addContainer(new_extruder) - ## Create a new Extruder stack - # - # \param new_stack_id The ID of the new stack. - # \param extruder_definition The definition to base the new stack on. - # \param machine_definition_id The ID of the machine definition to use for the user container. - # \param position The position the extruder occupies in the machine. - # \param variant_container The variant selected for the current extruder. - # \param material_container The material selected for the current extruder. - # \param quality_container The quality selected for the current extruder. - # - # \return A new Extruder stack instance with the specified parameters. @classmethod def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface, machine_definition_id: str, @@ -139,6 +133,19 @@ class CuraStackBuilder: material_container: "InstanceContainer", quality_container: "InstanceContainer") -> ExtruderStack: + """Create a new Extruder stack + + :param new_stack_id: The ID of the new stack. + :param extruder_definition: The definition to base the new stack on. + :param machine_definition_id: The ID of the machine definition to use for the user container. + :param position: The position the extruder occupies in the machine. + :param variant_container: The variant selected for the current extruder. + :param material_container: The material selected for the current extruder. + :param quality_container: The quality selected for the current extruder. + + :return: A new Extruder stack instance with the specified parameters. + """ + from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() registry = application.getContainerRegistry() @@ -167,29 +174,23 @@ class CuraStackBuilder: return stack - ## Create a new Global stack - # - # \param new_stack_id The ID of the new stack. - # \param definition The definition to base the new stack on. - # \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm" - # - # \return A new Global stack instance with the specified parameters. - - ## Create a new Global stack - # - # \param new_stack_id The ID of the new stack. - # \param definition The definition to base the new stack on. - # \param variant_container The variant selected for the current stack. - # \param material_container The material selected for the current stack. - # \param quality_container The quality selected for the current stack. - # - # \return A new Global stack instance with the specified parameters. @classmethod def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface, variant_container: "InstanceContainer", material_container: "InstanceContainer", quality_container: "InstanceContainer") -> GlobalStack: + """Create a new Global stack + + :param new_stack_id: The ID of the new stack. + :param definition: The definition to base the new stack on. + :param variant_container: The variant selected for the current stack. + :param material_container: The material selected for the current stack. + :param quality_container: The quality selected for the current stack. + + :return: A new Global stack instance with the specified parameters. + """ + from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() registry = application.getContainerRegistry() diff --git a/cura/Settings/Exceptions.py b/cura/Settings/Exceptions.py index 0a869cf922..fbb130417c 100644 --- a/cura/Settings/Exceptions.py +++ b/cura/Settings/Exceptions.py @@ -2,21 +2,25 @@ # Cura is released under the terms of the LGPLv3 or higher. -## Raised when trying to perform an operation like add on a stack that does not allow that. class InvalidOperationError(Exception): + """Raised when trying to perform an operation like add on a stack that does not allow that.""" + pass -## Raised when trying to replace a container with a container that does not have the expected type. class InvalidContainerError(Exception): + """Raised when trying to replace a container with a container that does not have the expected type.""" + pass -## Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders. class TooManyExtrudersError(Exception): + """Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders.""" + pass -## Raised when an extruder has no next stack set. class NoGlobalStackError(Exception): + """Raised when an extruder has no next stack set.""" + pass diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 4610e6a454..2cc9ec4631 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -19,13 +19,15 @@ if TYPE_CHECKING: from cura.Settings.ExtruderStack import ExtruderStack -## Manages all existing extruder stacks. -# -# This keeps a list of extruder stacks for each machine. class ExtruderManager(QObject): + """Manages all existing extruder stacks. + + This keeps a list of extruder stacks for each machine. + """ - ## Registers listeners and such to listen to changes to the extruders. def __init__(self, parent = None): + """Registers listeners and such to listen to changes to the extruders.""" + if ExtruderManager.__instance is not None: raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) ExtruderManager.__instance = self @@ -43,20 +45,22 @@ class ExtruderManager(QObject): Selection.selectionChanged.connect(self.resetSelectedObjectExtruders) - ## Signal to notify other components when the list of extruders for a machine definition changes. extrudersChanged = pyqtSignal(QVariant) + """Signal to notify other components when the list of extruders for a machine definition changes.""" - ## Notify when the user switches the currently active extruder. activeExtruderChanged = pyqtSignal() + """Notify when the user switches the currently active extruder.""" - ## Gets the unique identifier of the currently active extruder stack. - # - # The currently active extruder stack is the stack that is currently being - # edited. - # - # \return The unique ID of the currently active extruder stack. @pyqtProperty(str, notify = activeExtruderChanged) def activeExtruderStackId(self) -> Optional[str]: + """Gets the unique identifier of the currently active extruder stack. + + The currently active extruder stack is the stack that is currently being + edited. + + :return: The unique ID of the currently active extruder stack. + """ + if not self._application.getGlobalContainerStack(): return None # No active machine, so no active extruder. try: @@ -64,9 +68,10 @@ class ExtruderManager(QObject): except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong. return None - ## Gets a dict with the extruder stack ids with the extruder number as the key. @pyqtProperty("QVariantMap", notify = extrudersChanged) def extruderIds(self) -> Dict[str, str]: + """Gets a dict with the extruder stack ids with the extruder number as the key.""" + extruder_stack_ids = {} # type: Dict[str, str] global_container_stack = self._application.getGlobalContainerStack() @@ -75,11 +80,13 @@ class ExtruderManager(QObject): return extruder_stack_ids - ## Changes the active extruder by index. - # - # \param index The index of the new active extruder. @pyqtSlot(int) def setActiveExtruderIndex(self, index: int) -> None: + """Changes the active extruder by index. + + :param index: The index of the new active extruder. + """ + if self._active_extruder_index != index: self._active_extruder_index = index self.activeExtruderChanged.emit() @@ -88,12 +95,13 @@ class ExtruderManager(QObject): def activeExtruderIndex(self) -> int: return self._active_extruder_index - ## Emitted whenever the selectedObjectExtruders property changes. selectedObjectExtrudersChanged = pyqtSignal() + """Emitted whenever the selectedObjectExtruders property changes.""" - ## Provides a list of extruder IDs used by the current selected objects. @pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged) def selectedObjectExtruders(self) -> List[Union[str, "ExtruderStack"]]: + """Provides a list of extruder IDs used by the current selected objects.""" + if not self._selected_object_extruders: object_extruders = set() @@ -122,11 +130,13 @@ class ExtruderManager(QObject): return self._selected_object_extruders - ## Reset the internal list used for the selectedObjectExtruders property - # - # This will trigger a recalculation of the extruders used for the - # selection. def resetSelectedObjectExtruders(self) -> None: + """Reset the internal list used for the selectedObjectExtruders property + + This will trigger a recalculation of the extruders used for the + selection. + """ + self._selected_object_extruders = [] self.selectedObjectExtrudersChanged.emit() @@ -134,8 +144,9 @@ class ExtruderManager(QObject): def getActiveExtruderStack(self) -> Optional["ExtruderStack"]: return self.getExtruderStack(self.activeExtruderIndex) - ## Get an extruder stack by index def getExtruderStack(self, index) -> Optional["ExtruderStack"]: + """Get an extruder stack by index""" + global_container_stack = self._application.getGlobalContainerStack() if global_container_stack: if global_container_stack.getId() in self._extruder_trains: @@ -143,31 +154,14 @@ class ExtruderManager(QObject): return self._extruder_trains[global_container_stack.getId()][str(index)] return None - def registerExtruder(self, extruder_train: "ExtruderStack", machine_id: str) -> None: - changed = False - - if machine_id not in self._extruder_trains: - self._extruder_trains[machine_id] = {} - changed = True - - # do not register if an extruder has already been registered at the position on this machine - if any(item.getId() == extruder_train.getId() for item in self._extruder_trains[machine_id].values()): - Logger.log("w", "Extruder [%s] has already been registered on machine [%s], not doing anything", - extruder_train.getId(), machine_id) - return - - if extruder_train: - self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train - changed = True - if changed: - self.extrudersChanged.emit(machine_id) - - ## Gets a property of a setting for all extruders. - # - # \param setting_key \type{str} The setting to get the property of. - # \param property \type{str} The property to get. - # \return \type{List} the list of results def getAllExtruderSettings(self, setting_key: str, prop: str) -> List[Any]: + """Gets a property of a setting for all extruders. + + :param setting_key: :type{str} The setting to get the property of. + :param prop: :type{str} The property to get. + :return: :type{List} the list of results + """ + result = [] for extruder_stack in self.getActiveExtruderStacks(): @@ -182,17 +176,19 @@ class ExtruderManager(QObject): else: return value - ## Gets the extruder stacks that are actually being used at the moment. - # - # An extruder stack is being used if it is the extruder to print any mesh - # with, or if it is the support infill extruder, the support interface - # extruder, or the bed adhesion extruder. - # - # If there are no extruders, this returns the global stack as a singleton - # list. - # - # \return A list of extruder stacks. def getUsedExtruderStacks(self) -> List["ExtruderStack"]: + """Gets the extruder stacks that are actually being used at the moment. + + An extruder stack is being used if it is the extruder to print any mesh + with, or if it is the support infill extruder, the support interface + extruder, or the bed adhesion extruder. + + If there are no extruders, this returns the global stack as a singleton + list. + + :return: A list of extruder stacks. + """ + global_stack = self._application.getGlobalContainerStack() container_registry = ContainerRegistry.getInstance() @@ -277,11 +273,13 @@ class ExtruderManager(QObject): Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids) return [] - ## Get the extruder that the print will start with. - # - # This should mirror the implementation in CuraEngine of - # ``FffGcodeWriter::getStartExtruder()``. def getInitialExtruderNr(self) -> int: + """Get the extruder that the print will start with. + + This should mirror the implementation in CuraEngine of + ``FffGcodeWriter::getStartExtruder()``. + """ + application = cura.CuraApplication.CuraApplication.getInstance() global_stack = application.getGlobalContainerStack() @@ -296,28 +294,35 @@ class ExtruderManager(QObject): # 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. - # - # \param machine_id The machine to remove the extruders for. def removeMachineExtruders(self, machine_id: str) -> None: + """Removes the container stack and user profile for the extruders for a specific machine. + + :param machine_id: The machine to remove the extruders for. + """ + for extruder in self.getMachineExtruders(machine_id): ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId()) + ContainerRegistry.getInstance().removeContainer(extruder.definitionChanges.getId()) ContainerRegistry.getInstance().removeContainer(extruder.getId()) if machine_id in self._extruder_trains: del self._extruder_trains[machine_id] - ## Returns extruders for a specific machine. - # - # \param machine_id The machine to get the extruders of. def getMachineExtruders(self, machine_id: str) -> List["ExtruderStack"]: + """Returns extruders for a specific machine. + + :param machine_id: The machine to get the extruders of. + """ + if machine_id not in self._extruder_trains: return [] return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]] - ## Returns the list of active extruder stacks, taking into account the machine extruder count. - # - # \return \type{List[ContainerStack]} a list of def getActiveExtruderStacks(self) -> List["ExtruderStack"]: + """Returns the list of active extruder stacks, taking into account the machine extruder count. + + :return: :type{List[ContainerStack]} a list of + """ + global_stack = self._application.getGlobalContainerStack() if not global_stack: return [] @@ -329,8 +334,9 @@ class ExtruderManager(QObject): self.resetSelectedObjectExtruders() - ## Adds the extruders to the selected machine. def addMachineExtruders(self, global_stack: GlobalStack) -> None: + """Adds the extruders to the selected machine.""" + extruders_changed = False container_registry = ContainerRegistry.getInstance() global_stack_id = global_stack.getId() @@ -396,26 +402,30 @@ class ExtruderManager(QObject): raise IndexError(msg) extruder_stack_0.definition = extruder_definition - ## Get all extruder values for a certain setting. - # - # This is exposed to qml for display purposes - # - # \param key The key of the setting to retrieve values for. - # - # \return String representing the extruder values @pyqtSlot(str, result="QVariant") def getInstanceExtruderValues(self, key: str) -> List: + """Get all extruder values for a certain setting. + + This is exposed to qml for display purposes + + :param key: The key of the setting to retrieve values for. + + :return: String representing the extruder values + """ + return self._application.getCuraFormulaFunctions().getValuesInAllExtruders(key) - ## Get the resolve value or value for a given key - # - # This is the effective value for a given key, it is used for values in the global stack. - # This is exposed to SettingFunction to use in value functions. - # \param key The key of the setting to get the value of. - # - # \return The effective value @staticmethod def getResolveOrValue(key: str) -> Any: + """Get the resolve value or value for a given key + + This is the effective value for a given key, it is used for values in the global stack. + This is exposed to SettingFunction to use in value functions. + :param key: The key of the setting to get the value of. + + :return: The effective value + """ + global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) resolved_value = global_stack.getProperty(key, "value") diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py index 5d4b3e38b1..bb35b336c7 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -22,10 +22,9 @@ if TYPE_CHECKING: from cura.Settings.GlobalStack import GlobalStack -## Represents an Extruder and its related containers. -# -# class ExtruderStack(CuraContainerStack): + """Represents an Extruder and its related containers.""" + def __init__(self, container_id: str) -> None: super().__init__(container_id) @@ -33,20 +32,21 @@ class ExtruderStack(CuraContainerStack): self.propertiesChanged.connect(self._onPropertiesChanged) + self.setDirty(False) + enabledChanged = pyqtSignal() - ## Overridden from ContainerStack - # - # This will set the next stack and ensure that we register this stack as an extruder. @override(ContainerStack) def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None: + """Overridden from ContainerStack + + This will set the next stack and ensure that we register this stack as an extruder. + """ + super().setNextStack(stack) stack.addExtruder(self) self.setMetaDataEntry("machine", stack.id) - # For backward compatibility: Register the extruder with the Extruder Manager - ExtruderManager.getInstance().registerExtruder(self, stack.id) - @override(ContainerStack) def getNextStack(self) -> Optional["GlobalStack"]: return super().getNextStack() @@ -71,11 +71,13 @@ class ExtruderStack(CuraContainerStack): compatibleMaterialDiameterChanged = pyqtSignal() - ## Return the filament diameter that the machine requires. - # - # If the machine has no requirement for the diameter, -1 is returned. - # \return The filament diameter for the printer def getCompatibleMaterialDiameter(self) -> float: + """Return the filament diameter that the machine requires. + + If the machine has no requirement for the diameter, -1 is returned. + :return: The filament diameter for the printer + """ + context = PropertyEvaluationContext(self) context.context["evaluate_from_container_index"] = _ContainerIndexes.Variant @@ -97,31 +99,35 @@ class ExtruderStack(CuraContainerStack): approximateMaterialDiameterChanged = pyqtSignal() - ## Return the approximate filament diameter that the machine requires. - # - # The approximate material diameter is the material diameter rounded to - # the nearest millimetre. - # - # If the machine has no requirement for the diameter, -1 is returned. - # - # \return The approximate filament diameter for the printer def getApproximateMaterialDiameter(self) -> float: + """Return the approximate filament diameter that the machine requires. + + The approximate material diameter is the material diameter rounded to + the nearest millimetre. + + If the machine has no requirement for the diameter, -1 is returned. + + :return: The approximate filament diameter for the printer + """ + return round(self.getCompatibleMaterialDiameter()) approximateMaterialDiameter = pyqtProperty(float, fget = getApproximateMaterialDiameter, notify = approximateMaterialDiameterChanged) - ## Overridden from ContainerStack - # - # It will perform a few extra checks when trying to get properties. - # - # The two extra checks it currently does is to ensure a next stack is set and to bypass - # the extruder when the property is not settable per extruder. - # - # \throws Exceptions.NoGlobalStackError Raised when trying to get a property from an extruder without - # having a next stack set. @override(ContainerStack) def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: + """Overridden from ContainerStack + + It will perform a few extra checks when trying to get properties. + + The two extra checks it currently does is to ensure a next stack is set and to bypass + the extruder when the property is not settable per extruder. + + :throws Exceptions.NoGlobalStackError Raised when trying to get a property from an extruder without + having a next stack set. + """ + if not self._next_stack: raise Exceptions.NoGlobalStackError("Extruder {id} is missing the next stack!".format(id = self.id)) diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py index d3a8842aa3..a9164d0fb9 100755 --- a/cura/Settings/GlobalStack.py +++ b/cura/Settings/GlobalStack.py @@ -29,9 +29,9 @@ if TYPE_CHECKING: from cura.Settings.ExtruderStack import ExtruderStack -## Represents the Global or Machine stack and its related containers. -# class GlobalStack(CuraContainerStack): + """Represents the Global or Machine stack and its related containers.""" + def __init__(self, container_id: str) -> None: super().__init__(container_id) @@ -55,15 +55,19 @@ class GlobalStack(CuraContainerStack): # properties. So we need to tie them together like this. self.metaDataChanged.connect(self.configuredConnectionTypesChanged) + self.setDirty(False) + extrudersChanged = pyqtSignal() configuredConnectionTypesChanged = pyqtSignal() - ## Get the list of extruders of this stack. - # - # \return The extruders registered with this stack. @pyqtProperty("QVariantMap", notify = extrudersChanged) @deprecated("Please use extruderList instead.", "4.4") def extruders(self) -> Dict[str, "ExtruderStack"]: + """Get the list of extruders of this stack. + + :return: The extruders registered with this stack. + """ + return self._extruders @pyqtProperty("QVariantList", notify = extrudersChanged) @@ -86,16 +90,18 @@ class GlobalStack(CuraContainerStack): def getLoadingPriority(cls) -> int: return 2 - ## The configured connection types can be used to find out if the global - # stack is configured to be connected with a printer, without having to - # know all the details as to how this is exactly done (and without - # actually setting the stack to be active). - # - # This data can then in turn also be used when the global stack is active; - # If we can't get a network connection, but it is configured to have one, - # we can display a different icon to indicate the difference. @pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged) def configuredConnectionTypes(self) -> List[int]: + """The configured connection types can be used to find out if the global + + stack is configured to be connected with a printer, without having to + know all the details as to how this is exactly done (and without + actually setting the stack to be active). + + This data can then in turn also be used when the global stack is active; + If we can't get a network connection, but it is configured to have one, + we can display a different icon to indicate the difference. + """ # Requesting it from the metadata actually gets them as strings (as that's what you get from serializing). # But we do want them returned as a list of ints (so the rest of the code can directly compare) connection_types = self.getMetaDataEntry("connection_type", "").split(",") @@ -122,16 +128,18 @@ class GlobalStack(CuraContainerStack): ConnectionType.CloudConnection.value] return has_remote_connection - ## \sa configuredConnectionTypes def addConfiguredConnectionType(self, connection_type: int) -> None: + """:sa configuredConnectionTypes""" + configured_connection_types = self.configuredConnectionTypes if connection_type not in configured_connection_types: # Store the values as a string. configured_connection_types.append(connection_type) self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types])) - ## \sa configuredConnectionTypes def removeConfiguredConnectionType(self, connection_type: int) -> None: + """:sa configuredConnectionTypes""" + configured_connection_types = self.configuredConnectionTypes if connection_type in configured_connection_types: # Store the values as a string. @@ -163,13 +171,15 @@ class GlobalStack(CuraContainerStack): def preferred_output_file_formats(self) -> str: return self.getMetaDataEntry("file_formats") - ## Add an extruder to the list of extruders of this stack. - # - # \param extruder The extruder to add. - # - # \throws Exceptions.TooManyExtrudersError Raised when trying to add an extruder while we - # already have the maximum number of extruders. def addExtruder(self, extruder: ContainerStack) -> None: + """Add an extruder to the list of extruders of this stack. + + :param extruder: The extruder to add. + + :raise Exceptions.TooManyExtrudersError: Raised when trying to add an extruder while we + already have the maximum number of extruders. + """ + position = extruder.getMetaDataEntry("position") if position is None: Logger.log("w", "No position defined for extruder {extruder}, cannot add it to stack {stack}", extruder = extruder.id, stack = self.id) @@ -183,19 +193,21 @@ class GlobalStack(CuraContainerStack): self.extrudersChanged.emit() Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position) - ## Overridden from ContainerStack - # - # This will return the value of the specified property for the specified setting, - # unless the property is "value" and that setting has a "resolve" function set. - # When a resolve is set, it will instead try and execute the resolve first and - # then fall back to the normal "value" property. - # - # \param key The setting key to get the property of. - # \param property_name The property to get the value of. - # - # \return The value of the property for the specified setting, or None if not found. @override(ContainerStack) def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: + """Overridden from ContainerStack + + This will return the value of the specified property for the specified setting, + unless the property is "value" and that setting has a "resolve" function set. + When a resolve is set, it will instead try and execute the resolve first and + then fall back to the normal "value" property. + + :param key: The setting key to get the property of. + :param property_name: The property to get the value of. + + :return: The value of the property for the specified setting, or None if not found. + """ + if not self.definition.findDefinitions(key = key): return None @@ -235,11 +247,13 @@ class GlobalStack(CuraContainerStack): context.popContainer() return result - ## Overridden from ContainerStack - # - # This will simply raise an exception since the Global stack cannot have a next stack. @override(ContainerStack) def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None: + """Overridden from ContainerStack + + This will simply raise an exception since the Global stack cannot have a next stack. + """ + raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!") # protected: @@ -267,9 +281,11 @@ class GlobalStack(CuraContainerStack): return True - ## Perform some sanity checks on the global stack - # Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1 def isValid(self) -> bool: + """Perform some sanity checks on the global stack + + Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1 + """ container_registry = ContainerRegistry.getInstance() extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = self.getId()) @@ -299,9 +315,10 @@ class GlobalStack(CuraContainerStack): def hasVariantBuildplates(self) -> bool: return parseBool(self.getMetaDataEntry("has_variant_buildplates", False)) - ## Get default firmware file name if one is specified in the firmware @pyqtSlot(result = str) def getDefaultFirmwareName(self) -> str: + """Get default firmware file name if one is specified in the firmware""" + machine_has_heated_bed = self.getProperty("machine_heated_bed", "value") baudrate = 250000 diff --git a/cura/Settings/IntentManager.py b/cura/Settings/IntentManager.py index 10ea8dff6a..a556a86dd8 100644 --- a/cura/Settings/IntentManager.py +++ b/cura/Settings/IntentManager.py @@ -15,29 +15,32 @@ if TYPE_CHECKING: from UM.Settings.InstanceContainer import InstanceContainer -## Front-end for querying which intents are available for a certain -# configuration. class IntentManager(QObject): + """Front-end for querying which intents are available for a certain configuration. + """ __instance = None - ## This class is a singleton. @classmethod def getInstance(cls): + """This class is a singleton.""" + if not cls.__instance: cls.__instance = IntentManager() return cls.__instance intentCategoryChanged = pyqtSignal() #Triggered when we switch categories. - ## Gets the metadata dictionaries of all intent profiles for a given - # configuration. - # - # \param definition_id ID of the printer. - # \param nozzle_name Name of the nozzle. - # \param material_base_file The base_file of the material. - # \return A list of metadata dictionaries matching the search criteria, or - # an empty list if nothing was found. def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]: + """Gets the metadata dictionaries of all intent profiles for a given + + configuration. + + :param definition_id: ID of the printer. + :param nozzle_name: Name of the nozzle. + :param material_base_file: The base_file of the material. + :return: A list of metadata dictionaries matching the search criteria, or + an empty list if nothing was found. + """ intent_metadatas = [] # type: List[Dict[str, Any]] try: materials = ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials @@ -53,28 +56,32 @@ class IntentManager(QObject): intent_metadatas.append(intent_node.getMetadata()) return intent_metadatas - ## Collects and returns all intent categories available for the given - # parameters. Note that the 'default' category is always available. - # - # \param definition_id ID of the printer. - # \param nozzle_name Name of the nozzle. - # \param material_id ID of the material. - # \return A set of intent category names. def intentCategories(self, definition_id: str, nozzle_id: str, material_id: str) -> List[str]: + """Collects and returns all intent categories available for the given + + parameters. Note that the 'default' category is always available. + + :param definition_id: ID of the printer. + :param nozzle_name: Name of the nozzle. + :param material_id: ID of the material. + :return: A set of intent category names. + """ categories = set() for intent in self.intentMetadatas(definition_id, nozzle_id, material_id): categories.add(intent["intent_category"]) categories.add("default") #The "empty" intent is not an actual profile specific to the configuration but we do want it to appear in the categories list. return list(categories) - ## List of intents to be displayed in the interface. - # - # For the interface this will have to be broken up into the different - # intent categories. That is up to the model there. - # - # \return A list of tuples of intent_category and quality_type. The actual - # instance may vary per extruder. def getCurrentAvailableIntents(self) -> List[Tuple[str, str]]: + """List of intents to be displayed in the interface. + + For the interface this will have to be broken up into the different + intent categories. That is up to the model there. + + :return: A list of tuples of intent_category and quality_type. The actual + instance may vary per extruder. + """ + application = cura.CuraApplication.CuraApplication.getInstance() global_stack = application.getGlobalContainerStack() if global_stack is None: @@ -100,16 +107,18 @@ class IntentManager(QObject): result.add((intent_metadata["intent_category"], intent_metadata["quality_type"])) return list(result) - ## List of intent categories available in either of the extruders. - # - # This is purposefully inconsistent with the way that the quality types - # are listed. The quality types will show all quality types available in - # the printer using any configuration. This will only list the intent - # categories that are available using the current configuration (but the - # union over the extruders). - # \return List of all categories in the current configurations of all - # extruders. def currentAvailableIntentCategories(self) -> List[str]: + """List of intent categories available in either of the extruders. + + This is purposefully inconsistent with the way that the quality types + are listed. The quality types will show all quality types available in + the printer using any configuration. This will only list the intent + categories that are available using the current configuration (but the + union over the extruders). + :return: List of all categories in the current configurations of all + extruders. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return ["default"] @@ -123,10 +132,12 @@ class IntentManager(QObject): final_intent_categories.update(self.intentCategories(current_definition_id, nozzle_name, material_id)) return list(final_intent_categories) - ## The intent that gets selected by default when no intent is available for - # the configuration, an extruder can't match the intent that the user - # selects, or just when creating a new printer. def getDefaultIntent(self) -> "InstanceContainer": + """The intent that gets selected by default when no intent is available for + + the configuration, an extruder can't match the intent that the user + selects, or just when creating a new printer. + """ return empty_intent_container @pyqtProperty(str, notify = intentCategoryChanged) @@ -137,9 +148,10 @@ class IntentManager(QObject): return "" return active_extruder_stack.intent.getMetaDataEntry("intent_category", "") - ## Apply intent on the stacks. @pyqtSlot(str, str) def selectIntent(self, intent_category: str, quality_type: str) -> None: + """Apply intent on the stacks.""" + Logger.log("i", "Attempting to set intent_category to [%s] and quality type to [%s]", intent_category, quality_type) old_intent_category = self.currentIntentCategory application = cura.CuraApplication.CuraApplication.getInstance() diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 2866e3a494..1934befd66 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -22,6 +22,7 @@ from UM.Settings.SettingFunction import SettingFunction from UM.Signal import postponeSignals, CompressTechnique import cura.CuraApplication # Imported like this to prevent circular references. +from UM.Util import parseBool from cura.Machines.ContainerNode import ContainerNode from cura.Machines.ContainerTree import ContainerTree @@ -37,6 +38,7 @@ from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.cura_empty_instance_containers import (empty_definition_changes_container, empty_variant_container, empty_material_container, empty_quality_container, empty_quality_changes_container, empty_intent_container) +from cura.UltimakerCloud.UltimakerCloudConstants import META_UM_LINKED_TO_ACCOUNT from .CuraStackBuilder import CuraStackBuilder @@ -215,8 +217,9 @@ class MachineManager(QObject): return set() return general_definition_containers[0].getAllKeys() - ## Triggered when the global container stack is changed in CuraApplication. def _onGlobalContainerChanged(self) -> None: + """Triggered when the global container stack is changed in CuraApplication.""" + if self._global_container_stack: try: self._global_container_stack.containersChanged.disconnect(self._onContainersChanged) @@ -287,9 +290,15 @@ class MachineManager(QObject): self.activeStackValueChanged.emit() @pyqtSlot(str) - def setActiveMachine(self, stack_id: str) -> None: + def setActiveMachine(self, stack_id: Optional[str]) -> None: self.blurSettings.emit() # Ensure no-one has focus. + if not stack_id: + self._application.setGlobalContainerStack(None) + self.globalContainerChanged.emit() + self._application.showAddPrintersUncancellableDialog.emit() + return + container_registry = CuraContainerRegistry.getInstance() containers = container_registry.findContainerStacks(id = stack_id) if not containers: @@ -338,12 +347,15 @@ class MachineManager(QObject): Logger.log("w", "An extruder has an unknown material, switching it to the preferred material") self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material) - ## Given a definition id, return the machine with this id. - # Optional: add a list of keys and values to filter the list of machines with the given definition id - # \param definition_id \type{str} definition id that needs to look for - # \param metadata_filter \type{dict} list of metadata keys and values used for filtering @staticmethod def getMachine(definition_id: str, metadata_filter: Optional[Dict[str, str]] = None) -> Optional["GlobalStack"]: + """Given a definition id, return the machine with this id. + + Optional: add a list of keys and values to filter the list of machines with the given definition id + :param definition_id: :type{str} definition id that needs to look for + :param metadata_filter: :type{dict} list of metadata keys and values used for filtering + """ + if metadata_filter is None: metadata_filter = {} machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) @@ -397,9 +409,10 @@ class MachineManager(QObject): Logger.log("d", "Checking %s stacks for errors took %.2f s" % (count, time.time() - time_start)) return False - ## Check if the global_container has instances in the user container @pyqtProperty(bool, notify = activeStackValueChanged) def hasUserSettings(self) -> bool: + """Check if the global_container has instances in the user container""" + if not self._global_container_stack: return False @@ -422,10 +435,12 @@ class MachineManager(QObject): num_user_settings += stack.getTop().getNumInstances() return num_user_settings - ## Delete a user setting from the global stack and all extruder stacks. - # \param key \type{str} the name of the key to delete @pyqtSlot(str) def clearUserSettingAllCurrentStacks(self, key: str) -> None: + """Delete a user setting from the global stack and all extruder stacks. + + :param key: :type{str} the name of the key to delete + """ Logger.log("i", "Clearing the setting [%s] from all stacks", key) if not self._global_container_stack: return @@ -454,11 +469,13 @@ class MachineManager(QObject): for container in send_emits_containers: container.sendPostponedEmits() - ## Check if none of the stacks contain error states - # Note that the _stacks_have_errors is cached due to performance issues - # Calling _checkStack(s)ForErrors on every change is simply too expensive @pyqtProperty(bool, notify = stacksValidationChanged) def stacksHaveErrors(self) -> bool: + """Check if none of the stacks contain error states + + Note that the _stacks_have_errors is cached due to performance issues + Calling _checkStack(s)ForErrors on every change is simply too expensive + """ return bool(self._stacks_have_errors) @pyqtProperty(str, notify = globalContainerChanged) @@ -479,7 +496,15 @@ class MachineManager(QObject): @pyqtProperty(bool, notify = printerConnectedStatusChanged) def activeMachineIsGroup(self) -> bool: - return bool(self._printer_output_devices) and len(self._printer_output_devices[0].printers) > 1 + if self.activeMachine is None: + return False + + group_size = int(self.activeMachine.getMetaDataEntry("group_size", "-1")) + return group_size > 1 + + @pyqtProperty(bool, notify = printerConnectedStatusChanged) + def activeMachineIsLinkedToCurrentAccount(self) -> bool: + return parseBool(self.activeMachine.getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "True")) @pyqtProperty(bool, notify = printerConnectedStatusChanged) def activeMachineHasNetworkConnection(self) -> bool: @@ -490,7 +515,11 @@ class MachineManager(QObject): def activeMachineHasCloudConnection(self) -> bool: # A cloud connection is only available if any output device actually is a cloud connected device. return any(d.connectionType == ConnectionType.CloudConnection for d in self._printer_output_devices) - + + @pyqtProperty(bool, notify = printerConnectedStatusChanged) + def activeMachineHasCloudRegistration(self) -> bool: + return self.activeMachine is not None and ConnectionType.CloudConnection in self.activeMachine.configuredConnectionTypes + @pyqtProperty(bool, notify = printerConnectedStatusChanged) def activeMachineIsUsingCloudConnection(self) -> bool: return self.activeMachineHasCloudConnection and not self.activeMachineHasNetworkConnection @@ -528,14 +557,16 @@ class MachineManager(QObject): return material.getId() return "" - ## Gets the layer height of the currently active quality profile. - # - # This is indicated together with the name of the active quality profile. - # - # \return The layer height of the currently active quality profile. If - # there is no quality profile, this returns the default layer height. @pyqtProperty(float, notify = activeQualityGroupChanged) def activeQualityLayerHeight(self) -> float: + """Gets the layer height of the currently active quality profile. + + This is indicated together with the name of the active quality profile. + + :return: The layer height of the currently active quality profile. If + there is no quality profile, this returns the default layer height. + """ + if not self._global_container_stack: return 0 value = self._global_container_stack.getRawProperty("layer_height", "value", skip_until_container = self._global_container_stack.qualityChanges.getId()) @@ -605,13 +636,15 @@ class MachineManager(QObject): return result - ## Returns whether there is anything unsupported in the current set-up. - # - # The current set-up signifies the global stack and all extruder stacks, - # so this indicates whether there is any container in any of the container - # stacks that is not marked as supported. @pyqtProperty(bool, notify = activeQualityChanged) def isCurrentSetupSupported(self) -> bool: + """Returns whether there is anything unsupported in the current set-up. + + The current set-up signifies the global stack and all extruder stacks, + so this indicates whether there is any container in any of the container + stacks that is not marked as supported. + """ + if not self._global_container_stack: return False for stack in [self._global_container_stack] + self._global_container_stack.extruderList: @@ -622,9 +655,10 @@ class MachineManager(QObject): return False return True - ## Copy the value of the setting of the current extruder to all other extruders as well as the global container. @pyqtSlot(str) def copyValueToExtruders(self, key: str) -> None: + """Copy the value of the setting of the current extruder to all other extruders as well as the global container.""" + if self._active_container_stack is None or self._global_container_stack is None: return new_value = self._active_container_stack.getProperty(key, "value") @@ -634,9 +668,10 @@ class MachineManager(QObject): 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 - ## Copy the value of all manually changed settings of the current extruder to all other extruders. @pyqtSlot() def copyAllValuesToExtruders(self) -> None: + """Copy the value of all manually changed settings of the current extruder to all other extruders.""" + if self._active_container_stack is None or self._global_container_stack is None: return @@ -648,19 +683,23 @@ class MachineManager(QObject): # Check if the value has to be replaced extruder_stack.userChanges.setProperty(key, "value", new_value) - ## Get the Definition ID to use to select quality profiles for the currently active machine - # \returns DefinitionID (string) if found, empty string otherwise @pyqtProperty(str, notify = globalContainerChanged) def activeQualityDefinitionId(self) -> str: + """Get the Definition ID to use to select quality profiles for the currently active machine + + :returns: DefinitionID (string) if found, empty string otherwise + """ global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if not global_stack: return "" return ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition - ## Gets how the active definition calls variants - # Caveat: per-definition-variant-title is currently not translated (though the fallback is) @pyqtProperty(str, notify = globalContainerChanged) def activeDefinitionVariantsName(self) -> str: + """Gets how the active definition calls variants + + Caveat: per-definition-variant-title is currently not translated (though the fallback is) + """ fallback_title = catalog.i18nc("@label", "Nozzle") if self._global_container_stack: return self._global_container_stack.definition.getMetaDataEntry("variants_name", fallback_title) @@ -688,6 +727,8 @@ class MachineManager(QObject): other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id] if other_machine_stacks: self.setActiveMachine(other_machine_stacks[0]["id"]) + else: + self.setActiveMachine(None) metadatas = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id) if not metadatas: @@ -697,20 +738,24 @@ class MachineManager(QObject): containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id) for container in containers: CuraContainerRegistry.getInstance().removeContainer(container["id"]) + machine_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", name = machine_id) + if machine_stacks: + CuraContainerRegistry.getInstance().removeContainer(machine_stacks[0].definitionChanges.getId()) CuraContainerRegistry.getInstance().removeContainer(machine_id) # If the printer that is being removed is a network printer, the hidden printers have to be also removed group_id = metadata.get("group_id", None) if group_id: - metadata_filter = {"group_id": group_id} + metadata_filter = {"group_id": group_id, "hidden": True} hidden_containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) if hidden_containers: # This reuses the method and remove all printers recursively self.removeMachine(hidden_containers[0].getId()) - ## The selected buildplate is compatible if it is compatible with all the materials in all the extruders @pyqtProperty(bool, notify = activeMaterialChanged) def variantBuildplateCompatible(self) -> bool: + """The selected buildplate is compatible if it is compatible with all the materials in all the extruders""" + if not self._global_container_stack: return True @@ -727,10 +772,12 @@ class MachineManager(QObject): return buildplate_compatible - ## The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible - # for the other material but the buildplate is still usable @pyqtProperty(bool, notify = activeMaterialChanged) def variantBuildplateUsable(self) -> bool: + """The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible + + for the other material but the buildplate is still usable + """ if not self._global_container_stack: return True @@ -751,11 +798,13 @@ class MachineManager(QObject): return result - ## Get the Definition ID of a machine (specified by ID) - # \param machine_id string machine id to get the definition ID of - # \returns DefinitionID if found, None otherwise @pyqtSlot(str, result = str) def getDefinitionByMachineId(self, machine_id: str) -> Optional[str]: + """Get the Definition ID of a machine (specified by ID) + + :param machine_id: string machine id to get the definition ID of + :returns: DefinitionID if found, None otherwise + """ containers = CuraContainerRegistry.getInstance().findContainerStacks(id = machine_id) if containers: return containers[0].definition.getId() @@ -786,8 +835,9 @@ class MachineManager(QObject): Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value) return result - ## Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed def correctExtruderSettings(self) -> None: + """Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed""" + if self._global_container_stack is None: return for setting_key in self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.userChanges): @@ -803,9 +853,11 @@ class MachineManager(QObject): title = catalog.i18nc("@info:title", "Settings updated")) caution_message.show() - ## Set the amount of extruders on the active machine (global stack) - # \param extruder_count int the number of extruders to set def setActiveMachineExtruderCount(self, extruder_count: int) -> None: + """Set the amount of extruders on the active machine (global stack) + + :param extruder_count: int the number of extruders to set + """ if self._global_container_stack is None: return extruder_manager = self._application.getExtruderManager() @@ -902,9 +954,10 @@ class MachineManager(QObject): def defaultExtruderPosition(self) -> str: return self._default_extruder_position - ## This will fire the propertiesChanged for all settings so they will be updated in the front-end @pyqtSlot() def forceUpdateAllSettings(self) -> None: + """This will fire the propertiesChanged for all settings so they will be updated in the front-end""" + if self._global_container_stack is None: return with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): @@ -915,7 +968,7 @@ class MachineManager(QObject): @pyqtSlot(int, bool) def setExtruderEnabled(self, position: int, enabled: bool) -> None: - if self._global_container_stack is None or str(position) not in self._global_container_stack.extruders: + if self._global_container_stack is None or position >= len(self._global_container_stack.extruderList): Logger.log("w", "Could not find extruder on position %s.", position) return extruder = self._global_container_stack.extruderList[position] @@ -945,11 +998,13 @@ class MachineManager(QObject): def _onMaterialNameChanged(self) -> None: self.activeMaterialChanged.emit() - ## Get the signals that signal that the containers changed for all stacks. - # - # This includes the global stack and all extruder stacks. So if any - # container changed anywhere. def _getContainerChangedSignals(self) -> List[Signal]: + """Get the signals that signal that the containers changed for all stacks. + + This includes the global stack and all extruder stacks. So if any + container changed anywhere. + """ + if self._global_container_stack is None: return [] return [s.containersChanged for s in self._global_container_stack.extruderList + [self._global_container_stack]] @@ -962,18 +1017,21 @@ class MachineManager(QObject): container = extruder.userChanges container.setProperty(setting_name, property_name, property_value) - ## Reset all setting properties of a setting for all extruders. - # \param setting_name The ID of the setting to reset. @pyqtSlot(str) def resetSettingForAllExtruders(self, setting_name: str) -> None: + """Reset all setting properties of a setting for all extruders. + + :param setting_name: The ID of the setting to reset. + """ if self._global_container_stack is None: return for extruder in self._global_container_stack.extruderList: container = extruder.userChanges container.removeInstance(setting_name) - ## Update _current_root_material_id when the current root material was changed. def _onRootMaterialChanged(self) -> None: + """Update _current_root_material_id when the current root material was changed.""" + self._current_root_material_id = {} changed = False @@ -1073,14 +1131,14 @@ class MachineManager(QObject): self._global_container_stack.quality = quality_container self._global_container_stack.qualityChanges = quality_changes_container - for position, extruder in self._global_container_stack.extruders.items(): + for position, extruder in enumerate(self._global_container_stack.extruderList): quality_node = None if quality_group is not None: - quality_node = quality_group.nodes_for_extruders.get(int(position)) + quality_node = quality_group.nodes_for_extruders.get(position) quality_changes_container = empty_quality_changes_container quality_container = empty_quality_container - quality_changes_metadata = quality_changes_group.metadata_per_extruder.get(int(position)) + quality_changes_metadata = quality_changes_group.metadata_per_extruder.get(position) if quality_changes_metadata: containers = container_registry.findContainers(id = quality_changes_metadata["id"]) if containers: @@ -1099,7 +1157,7 @@ class MachineManager(QObject): def _setVariantNode(self, position: str, variant_node: "VariantNode") -> None: if self._global_container_stack is None: return - self._global_container_stack.extruders[position].variant = variant_node.container + self._global_container_stack.extruderList[int(position)].variant = variant_node.container self.activeVariantChanged.emit() def _setGlobalVariant(self, container_node: "ContainerNode") -> None: @@ -1114,7 +1172,7 @@ class MachineManager(QObject): return if material_node and material_node.container: material_container = material_node.container - self._global_container_stack.extruders[position].material = material_container + self._global_container_stack.extruderList[int(position)].material = material_container root_material_id = material_container.getMetaDataEntry("base_file", None) else: self._global_container_stack.extruderList[int(position)].material = empty_material_container @@ -1135,8 +1193,9 @@ class MachineManager(QObject): return False return True - ## Update current quality type and machine after setting material def _updateQualityWithMaterial(self, *args: Any) -> None: + """Update current quality type and machine after setting material""" + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return @@ -1177,8 +1236,9 @@ class MachineManager(QObject): current_quality_type, quality_type) self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True) - ## Update the current intent after the quality changed def _updateIntentWithQuality(self): + """Update the current intent after the quality changed""" + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return @@ -1205,16 +1265,18 @@ class MachineManager(QObject): category = current_category self.setIntentByCategory(category) - ## Update the material profile in the current stacks when the variant is - # changed. - # \param position The extruder stack to update. If provided with None, all - # extruder stacks will be updated. @pyqtSlot() def updateMaterialWithVariant(self, position: Optional[str] = None) -> None: + """Update the material profile in the current stacks when the variant is + + changed. + :param position: The extruder stack to update. If provided with None, all + extruder stacks will be updated. + """ if self._global_container_stack is None: return if position is None: - position_list = list(self._global_container_stack.extruders.keys()) + position_list = [str(position) for position in range(len(self._global_container_stack.extruderList))] else: position_list = [position] @@ -1245,10 +1307,12 @@ class MachineManager(QObject): material_node = nozzle_node.preferredMaterial(approximate_material_diameter) self._setMaterial(position_item, material_node) - ## Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new - # instance with the same network key. @pyqtSlot(str) def switchPrinterType(self, machine_name: str) -> None: + """Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new + + instance with the same network key. + """ # Don't switch if the user tries to change to the same type of printer if self._global_container_stack is None or self._global_container_stack.definition.name == machine_name: return @@ -1268,7 +1332,7 @@ class MachineManager(QObject): if not new_machine: Logger.log("e", "Failed to create new machine when switching configuration.") return - + for metadata_key in self._global_container_stack.getMetaData(): if metadata_key in new_machine.getMetaData(): continue # Don't copy the already preset stuff. @@ -1286,17 +1350,15 @@ class MachineManager(QObject): # Keep a temporary copy of the global and per-extruder user changes and transfer them to the user changes # of the new machine after the new_machine becomes active. global_user_changes = self._global_container_stack.userChanges - per_extruder_user_changes = {} - for extruder_name, extruder_stack in self._global_container_stack.extruders.items(): - per_extruder_user_changes[extruder_name] = extruder_stack.userChanges + per_extruder_user_changes = [extruder_stack.userChanges for extruder_stack in self._global_container_stack.extruderList] self.setActiveMachine(new_machine.getId()) # Apply the global and per-extruder userChanges to the new_machine (which is of different type than the # previous one). self._global_container_stack.setUserChanges(global_user_changes) - for extruder_name in self._global_container_stack.extruders.keys(): - self._global_container_stack.extruders[extruder_name].setUserChanges(per_extruder_user_changes[extruder_name]) + for i, user_changes in enumerate(per_extruder_user_changes): + self._global_container_stack.extruderList[i].setUserChanges(per_extruder_user_changes[i]) @pyqtSlot(QObject) def applyRemoteConfiguration(self, configuration: PrinterConfigurationModel) -> None: @@ -1307,7 +1369,6 @@ class MachineManager(QObject): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): self.switchPrinterType(configuration.printerType) - disabled_used_extruder_position_set = set() extruders_to_disable = set() # If an extruder that's currently used to print a model gets disabled due to the syncing, we need to show @@ -1316,8 +1377,8 @@ class MachineManager(QObject): for extruder_configuration in configuration.extruderConfigurations: # We support "" or None, since the cloud uses None instead of empty strings - extruder_has_hotend = extruder_configuration.hotendID and extruder_configuration.hotendID != "" - extruder_has_material = extruder_configuration.material.guid and extruder_configuration.material.guid != "" + extruder_has_hotend = extruder_configuration.hotendID not in ["", None] + extruder_has_material = extruder_configuration.material.guid not in [None, "", "00000000-0000-0000-0000-000000000000"] # If the machine doesn't have a hotend or material, disable this extruder if not extruder_has_hotend or not extruder_has_material: @@ -1335,7 +1396,6 @@ class MachineManager(QObject): self._global_container_stack.extruderList[int(position)].setEnabled(False) need_to_show_message = True - disabled_used_extruder_position_set.add(int(position)) else: machine_node = ContainerTree.getInstance().machines.get(self._global_container_stack.definition.getId()) @@ -1354,7 +1414,7 @@ class MachineManager(QObject): material_container_node = variant_node.materials.get(base_file, material_container_node) self._setMaterial(position, material_container_node) - self._global_container_stack.extruders[position].setEnabled(True) + self._global_container_stack.extruderList[int(position)].setEnabled(True) self.updateMaterialWithVariant(position) self.updateDefaultExtruder() @@ -1366,7 +1426,7 @@ class MachineManager(QObject): # Show human-readable extruder names such as "Extruder Left", "Extruder Front" instead of "Extruder 1, 2, 3". extruder_names = [] - for extruder_position in sorted(disabled_used_extruder_position_set): + for extruder_position in sorted(extruders_to_disable): extruder_stack = self._global_container_stack.extruderList[int(extruder_position)] extruder_name = extruder_stack.definition.getName() extruder_names.append(extruder_name) @@ -1400,12 +1460,14 @@ class MachineManager(QObject): material_node = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials[root_material_id] self.setMaterial(position, material_node) - ## Global_stack: if you want to provide your own global_stack instead of the current active one - # if you update an active machine, special measures have to be taken. @pyqtSlot(str, "QVariant") def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None: + """Global_stack: if you want to provide your own global_stack instead of the current active one + + if you update an active machine, special measures have to be taken. + """ if global_stack is not None and global_stack != self._global_container_stack: - global_stack.extruders[position].material = container_node.container + global_stack.extruderList[int(position)].material = container_node.container return position = str(position) self.blurSettings.emit() @@ -1449,10 +1511,12 @@ class MachineManager(QObject): # Get all the quality groups for this global stack and filter out by quality_type self.setQualityGroup(ContainerTree.getInstance().getCurrentQualityGroups()[quality_type]) - ## Optionally provide global_stack if you want to use your own - # The active global_stack is treated differently. @pyqtSlot(QObject) def setQualityGroup(self, quality_group: "QualityGroup", no_dialog: bool = False, global_stack: Optional["GlobalStack"] = None) -> None: + """Optionally provide global_stack if you want to use your own + + The active global_stack is treated differently. + """ if global_stack is not None and global_stack != self._global_container_stack: if quality_group is None: Logger.log("e", "Could not set quality group because quality group is None") @@ -1514,15 +1578,17 @@ class MachineManager(QObject): return {"main": main_part, "suffix": suffix_part} - ## Change the intent category of the current printer. - # - # All extruders can change their profiles. If an intent profile is - # available with the desired intent category, that one will get chosen. - # Otherwise the intent profile will be left to the empty profile, which - # represents the "default" intent category. - # \param intent_category The intent category to change to. @pyqtSlot(str) def setIntentByCategory(self, intent_category: str) -> None: + """Change the intent category of the current printer. + + All extruders can change their profiles. If an intent profile is + available with the desired intent category, that one will get chosen. + Otherwise the intent profile will be left to the empty profile, which + represents the "default" intent category. + :param intent_category: The intent category to change to. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return @@ -1554,21 +1620,25 @@ class MachineManager(QObject): else: # No intent had the correct category. extruder.intent = empty_intent_container - ## Get the currently activated quality group. - # - # If no printer is added yet or the printer doesn't have quality profiles, - # this returns ``None``. - # \return The currently active quality group. def activeQualityGroup(self) -> Optional["QualityGroup"]: + """Get the currently activated quality group. + + If no printer is added yet or the printer doesn't have quality profiles, + this returns ``None``. + :return: The currently active quality group. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if not global_stack or global_stack.quality == empty_quality_container: return None return ContainerTree.getInstance().getCurrentQualityGroups().get(self.activeQualityType) - ## Get the name of the active quality group. - # \return The name of the active quality group. @pyqtProperty(str, notify = activeQualityGroupChanged) def activeQualityGroupName(self) -> str: + """Get the name of the active quality group. + + :return: The name of the active quality group. + """ quality_group = self.activeQualityGroup() if quality_group is None: return "" @@ -1590,7 +1660,7 @@ class MachineManager(QObject): return with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): self._setQualityGroup(self.activeQualityGroup()) - for stack in [self._global_container_stack] + list(self._global_container_stack.extruders.values()): + for stack in [self._global_container_stack] + self._global_container_stack.extruderList: stack.userChanges.clear() @pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged) @@ -1641,9 +1711,10 @@ class MachineManager(QObject): self.updateMaterialWithVariant(None) self._updateQualityWithMaterial() - ## This function will translate any printer type name to an abbreviated printer type name @pyqtSlot(str, result = str) def getAbbreviatedMachineName(self, machine_type_name: str) -> str: + """This function will translate any printer type name to an abbreviated printer type name""" + abbr_machine = "" for word in re.findall(r"[\w']+", machine_type_name): if word.lower() == "ultimaker": diff --git a/cura/Settings/MachineNameValidator.py b/cura/Settings/MachineNameValidator.py index acdda4b0a0..8ab8907355 100644 --- a/cura/Settings/MachineNameValidator.py +++ b/cura/Settings/MachineNameValidator.py @@ -10,10 +10,13 @@ from UM.Resources import Resources from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.InstanceContainer import InstanceContainer -## Are machine names valid? -# -# Performs checks based on the length of the name. + class MachineNameValidator(QObject): + """Are machine names valid? + + Performs checks based on the length of the name. + """ + def __init__(self, parent = None): super().__init__(parent) @@ -32,12 +35,13 @@ class MachineNameValidator(QObject): validationChanged = pyqtSignal() - ## Check if a specified machine name is allowed. - # - # \param name The machine name to check. - # \return ``QValidator.Invalid`` if it's disallowed, or - # ``QValidator.Acceptable`` if it's allowed. def validate(self, name): + """Check if a specified machine name is allowed. + + :param name: The machine name to check. + :return: ``QValidator.Invalid`` if it's disallowed, or ``QValidator.Acceptable`` if it's allowed. + """ + #Check for file name length of the current settings container (which is the longest file we're saving with the name). try: filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax @@ -50,9 +54,10 @@ class MachineNameValidator(QObject): return QValidator.Acceptable #All checks succeeded. - ## Updates the validation state of a machine name text field. @pyqtSlot(str) def updateValidation(self, new_name): + """Updates the validation state of a machine name text field.""" + is_valid = self.validate(new_name) if is_valid == QValidator.Acceptable: self.validation_regex = "^.*$" #Matches anything. diff --git a/cura/Settings/PerObjectContainerStack.py b/cura/Settings/PerObjectContainerStack.py index a4f1f6ed06..fa2b8f5ec9 100644 --- a/cura/Settings/PerObjectContainerStack.py +++ b/cura/Settings/PerObjectContainerStack.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. from typing import Any, Optional @@ -45,13 +45,13 @@ class PerObjectContainerStack(CuraContainerStack): if "original_limit_to_extruder" in context.context: limit_to_extruder = context.context["original_limit_to_extruder"] - if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in global_stack.extruders: + if limit_to_extruder is not None and limit_to_extruder != "-1" and int(limit_to_extruder) <= len(global_stack.extruderList): # set the original limit_to_extruder if this is the first stack that has a non-overridden limit_to_extruder if "original_limit_to_extruder" not in context.context: context.context["original_limit_to_extruder"] = limit_to_extruder if super().getProperty(key, "settable_per_extruder", context): - result = global_stack.extruders[str(limit_to_extruder)].getProperty(key, property_name, context) + result = global_stack.extruderList[int(limit_to_extruder)].getProperty(key, property_name, context) if result is not None: context.popContainer() return result diff --git a/cura/Settings/SetObjectExtruderOperation.py b/cura/Settings/SetObjectExtruderOperation.py index 25c1c6b759..63227c58e3 100644 --- a/cura/Settings/SetObjectExtruderOperation.py +++ b/cura/Settings/SetObjectExtruderOperation.py @@ -6,8 +6,10 @@ from UM.Operations.Operation import Operation from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -## Simple operation to set the extruder a certain object should be printed with. + class SetObjectExtruderOperation(Operation): + """Simple operation to set the extruder a certain object should be printed with.""" + def __init__(self, node: SceneNode, extruder_id: str) -> None: self._node = node self._extruder_id = extruder_id diff --git a/cura/Settings/SettingInheritanceManager.py b/cura/Settings/SettingInheritanceManager.py index 7db579bf3f..6179e76ab7 100644 --- a/cura/Settings/SettingInheritanceManager.py +++ b/cura/Settings/SettingInheritanceManager.py @@ -45,9 +45,10 @@ class SettingInheritanceManager(QObject): settingsWithIntheritanceChanged = pyqtSignal() - ## Get the keys of all children settings with an override. @pyqtSlot(str, result = "QStringList") def getChildrenKeysWithOverride(self, key: str) -> List[str]: + """Get the keys of all children settings with an override.""" + if self._global_container_stack is None: return [] definitions = self._global_container_stack.definition.findDefinitions(key=key) @@ -163,8 +164,9 @@ class SettingInheritanceManager(QObject): def settingsWithInheritanceWarning(self) -> List[str]: return self._settings_with_inheritance_warning - ## Check if a setting has an inheritance function that is overwritten def _settingIsOverwritingInheritance(self, key: str, stack: ContainerStack = None) -> bool: + """Check if a setting has an inheritance function that is overwritten""" + has_setting_function = False if not stack: stack = self._active_container_stack @@ -177,17 +179,19 @@ class SettingInheritanceManager(QObject): containers = [] # type: List[ContainerInterface] - ## Check if the setting has a user state. If not, it is never overwritten. has_user_state = stack.getProperty(key, "state") == InstanceState.User + """Check if the setting has a user state. If not, it is never overwritten.""" + if not has_user_state: return False - ## If a setting is not enabled, don't label it as overwritten (It's never visible anyway). + # If a setting is not enabled, don't label it as overwritten (It's never visible anyway). if not stack.getProperty(key, "enabled"): return False - ## Also check if the top container is not a setting function (this happens if the inheritance is restored). user_container = stack.getTop() + """Also check if the top container is not a setting function (this happens if the inheritance is restored).""" + if user_container and isinstance(user_container.getProperty(key, "value"), SettingFunction): return False diff --git a/cura/Settings/SettingOverrideDecorator.py b/cura/Settings/SettingOverrideDecorator.py index 03b4c181dd..1b5fb84f4a 100644 --- a/cura/Settings/SettingOverrideDecorator.py +++ b/cura/Settings/SettingOverrideDecorator.py @@ -15,21 +15,24 @@ from UM.Application import Application from cura.Settings.PerObjectContainerStack import PerObjectContainerStack from cura.Settings.ExtruderManager import ExtruderManager -## A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding -# the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by -# this stack still resolve. @signalemitter class SettingOverrideDecorator(SceneNodeDecorator): - ## Event indicating that the user selected a different extruder. - activeExtruderChanged = Signal() + """A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding + + the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by + this stack still resolve. + """ + activeExtruderChanged = Signal() + """Event indicating that the user selected a different extruder.""" - ## Non-printing meshes - # - # If these settings are True for any mesh, the mesh does not need a convex hull, - # and is sent to the slicer regardless of whether it fits inside the build volume. - # Note that Support Mesh is not in here because it actually generates - # g-code in the volume of the mesh. _non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"} + """Non-printing meshes + + If these settings are True for any mesh, the mesh does not need a convex hull, + and is sent to the slicer regardless of whether it fits inside the build volume. + Note that Support Mesh is not in here because it actually generates + g-code in the volume of the mesh. + """ _non_thumbnail_visible_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"} def __init__(self): @@ -56,11 +59,11 @@ class SettingOverrideDecorator(SceneNodeDecorator): return "SettingOverrideInstanceContainer-%s" % uuid.uuid1() def __deepcopy__(self, memo): - ## Create a fresh decorator object deep_copy = SettingOverrideDecorator() + """Create a fresh decorator object""" - ## Copy the instance instance_container = copy.deepcopy(self._stack.getContainer(0), memo) + """Copy the instance""" # A unique name must be added, or replaceContainer will not replace it instance_container.setMetaDataEntry("id", self._generateUniqueName()) @@ -78,22 +81,28 @@ class SettingOverrideDecorator(SceneNodeDecorator): return deep_copy - ## Gets the currently active extruder to print this object with. - # - # \return An extruder's container stack. def getActiveExtruder(self): + """Gets the currently active extruder to print this object with. + + :return: An extruder's container stack. + """ + return self._extruder_stack - ## Gets the signal that emits if the active extruder changed. - # - # This can then be accessed via a decorator. def getActiveExtruderChangedSignal(self): + """Gets the signal that emits if the active extruder changed. + + This can then be accessed via a decorator. + """ + return self.activeExtruderChanged - ## Gets the currently active extruders position - # - # \return An extruder's position, or None if no position info is available. def getActiveExtruderPosition(self): + """Gets the currently active extruders position + + :return: An extruder's position, or None if no position info is available. + """ + # for support_meshes, always use the support_extruder if self.getStack().getProperty("support_mesh", "value"): global_container_stack = Application.getInstance().getGlobalContainerStack() @@ -126,9 +135,11 @@ class SettingOverrideDecorator(SceneNodeDecorator): Application.getInstance().getBackend().needsSlicing() Application.getInstance().getBackend().tickle() - ## Makes sure that the stack upon which the container stack is placed is - # kept up to date. def _updateNextStack(self): + """Makes sure that the stack upon which the container stack is placed is + + kept up to date. + """ if self._extruder_stack: extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack) if extruder_stack: @@ -147,10 +158,12 @@ class SettingOverrideDecorator(SceneNodeDecorator): else: self._stack.setNextStack(Application.getInstance().getGlobalContainerStack()) - ## Changes the extruder with which to print this node. - # - # \param extruder_stack_id The new extruder stack to print with. def setActiveExtruder(self, extruder_stack_id): + """Changes the extruder with which to print this node. + + :param extruder_stack_id: The new extruder stack to print with. + """ + self._extruder_stack = extruder_stack_id self._updateNextStack() ExtruderManager.getInstance().resetSelectedObjectExtruders() diff --git a/cura/Snapshot.py b/cura/Snapshot.py index 353b5ae17c..6f12aa88ba 100644 --- a/cura/Snapshot.py +++ b/cura/Snapshot.py @@ -30,11 +30,17 @@ class Snapshot: return min_x, max_x, min_y, max_y - ## Return a QImage of the scene - # Uses PreviewPass that leaves out some elements - # Aspect ratio assumes a square @staticmethod def snapshot(width = 300, height = 300): + """Return a QImage of the scene + + Uses PreviewPass that leaves out some elements Aspect ratio assumes a square + + :param width: width of the aspect ratio default 300 + :param height: height of the aspect ratio default 300 + :return: None when there is no model on the build plate otherwise it will return an image + """ + scene = Application.getInstance().getController().getScene() active_camera = scene.getActiveCamera() render_width, render_height = active_camera.getWindowSize() diff --git a/cura/UI/AddPrinterPagesModel.py b/cura/UI/AddPrinterPagesModel.py index b06f220374..9b35dbcacc 100644 --- a/cura/UI/AddPrinterPagesModel.py +++ b/cura/UI/AddPrinterPagesModel.py @@ -10,12 +10,11 @@ from .WelcomePagesModel import WelcomePagesModel # class AddPrinterPagesModel(WelcomePagesModel): - def initialize(self) -> None: + def initialize(self, cancellable: bool = True) -> None: self._pages.append({"id": "add_network_or_local_printer", "page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"), "next_page_id": "machine_actions", "next_page_button_text": self._catalog.i18nc("@action:button", "Add"), - "previous_page_button_text": self._catalog.i18nc("@action:button", "Cancel"), }) self._pages.append({"id": "add_printer_by_ip", "page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"), @@ -30,6 +29,9 @@ class AddPrinterPagesModel(WelcomePagesModel): "page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"), "should_show_function": self.shouldShowMachineActions, }) + if cancellable: + self._pages[0]["previous_page_button_text"] = self._catalog.i18nc("@action:button", "Cancel") + self.setItems(self._pages) diff --git a/cura/UI/MachineActionManager.py b/cura/UI/MachineActionManager.py index 6efd3217a1..5e31de32c2 100644 --- a/cura/UI/MachineActionManager.py +++ b/cura/UI/MachineActionManager.py @@ -15,13 +15,15 @@ if TYPE_CHECKING: from cura.MachineAction import MachineAction -## Raised when trying to add an unknown machine action as a required action class UnknownMachineActionError(Exception): + """Raised when trying to add an unknown machine action as a required action""" + pass -## Raised when trying to add a machine action that does not have an unique key. class NotUniqueMachineActionError(Exception): + """Raised when trying to add a machine action that does not have an unique key.""" + pass @@ -71,9 +73,11 @@ class MachineActionManager(QObject): self._definition_ids_with_default_actions_added.add(definition_id) Logger.log("i", "Default machine actions added for machine definition [%s]", definition_id) - ## Add a required action to a machine - # Raises an exception when the action is not recognised. def addRequiredAction(self, definition_id: str, action_key: str) -> None: + """Add a required action to a machine + + Raises an exception when the action is not recognised. + """ if action_key in self._machine_actions: if definition_id in self._required_actions: if self._machine_actions[action_key] not in self._required_actions[definition_id]: @@ -83,8 +87,9 @@ class MachineActionManager(QObject): else: raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id)) - ## Add a supported action to a machine. def addSupportedAction(self, definition_id: str, action_key: str) -> None: + """Add a supported action to a machine.""" + if action_key in self._machine_actions: if definition_id in self._supported_actions: if self._machine_actions[action_key] not in self._supported_actions[definition_id]: @@ -94,8 +99,9 @@ class MachineActionManager(QObject): else: Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id) - ## Add an action to the first start list of a machine. def addFirstStartAction(self, definition_id: str, action_key: str) -> None: + """Add an action to the first start list of a machine.""" + if action_key in self._machine_actions: if definition_id in self._first_start_actions: self._first_start_actions[definition_id].append(self._machine_actions[action_key]) @@ -104,57 +110,69 @@ class MachineActionManager(QObject): else: Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id) - ## Add a (unique) MachineAction - # if the Key of the action is not unique, an exception is raised. def addMachineAction(self, action: "MachineAction") -> None: + """Add a (unique) MachineAction + + if the Key of the action is not unique, an exception is raised. + """ if action.getKey() not in self._machine_actions: self._machine_actions[action.getKey()] = action else: raise NotUniqueMachineActionError("MachineAction with key %s was already added. Actions must have unique keys.", action.getKey()) - ## Get all actions supported by given machine - # \param definition_id The ID of the definition you want the supported actions of - # \returns set of supported actions. @pyqtSlot(str, result = "QVariantList") def getSupportedActions(self, definition_id: str) -> List["MachineAction"]: + """Get all actions supported by given machine + + :param definition_id: The ID of the definition you want the supported actions of + :returns: set of supported actions. + """ if definition_id in self._supported_actions: return list(self._supported_actions[definition_id]) else: return list() - ## Get all actions required by given machine - # \param definition_id The ID of the definition you want the required actions of - # \returns set of required actions. def getRequiredActions(self, definition_id: str) -> List["MachineAction"]: + """Get all actions required by given machine + + :param definition_id: The ID of the definition you want the required actions of + :returns: set of required actions. + """ if definition_id in self._required_actions: return self._required_actions[definition_id] else: return list() - ## Get all actions that need to be performed upon first start of a given machine. - # Note that contrary to required / supported actions a list is returned (as it could be required to run the same - # action multiple times). - # \param definition_id The ID of the definition that you want to get the "on added" actions for. - # \returns List of actions. @pyqtSlot(str, result = "QVariantList") def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]: + """Get all actions that need to be performed upon first start of a given machine. + + Note that contrary to required / supported actions a list is returned (as it could be required to run the same + action multiple times). + :param definition_id: The ID of the definition that you want to get the "on added" actions for. + :returns: List of actions. + """ if definition_id in self._first_start_actions: return self._first_start_actions[definition_id] else: return [] - ## Remove Machine action from manager - # \param action to remove def removeMachineAction(self, action: "MachineAction") -> None: + """Remove Machine action from manager + + :param action: to remove + """ try: del self._machine_actions[action.getKey()] except KeyError: Logger.log("w", "Trying to remove MachineAction (%s) that was already removed", action.getKey()) - ## Get MachineAction by key - # \param key String of key to select - # \return Machine action if found, None otherwise def getMachineAction(self, key: str) -> Optional["MachineAction"]: + """Get MachineAction by key + + :param key: String of key to select + :return: Machine action if found, None otherwise + """ if key in self._machine_actions: return self._machine_actions[key] else: diff --git a/cura/UI/MachineSettingsManager.py b/cura/UI/MachineSettingsManager.py index 671bb0ece0..1d2604c3c9 100644 --- a/cura/UI/MachineSettingsManager.py +++ b/cura/UI/MachineSettingsManager.py @@ -60,7 +60,6 @@ class MachineSettingsManager(QObject): # In other words: only continue for the UM2 (extended), but not for the UM2+ return - extruder_positions = list(global_stack.extruders.keys()) has_materials = global_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode" material_node = None @@ -73,12 +72,11 @@ class MachineSettingsManager(QObject): global_stack.removeMetaDataEntry("has_materials") # set materials - for position in extruder_positions: + for position, extruder in enumerate(global_stack.extruderList): if has_materials: - extruder = global_stack.extruderList[int(position)] approximate_diameter = extruder.getApproximateMaterialDiameter() variant_node = ContainerTree.getInstance().machines[global_stack.definition.getId()].variants[extruder.variant.getName()] material_node = variant_node.preferredMaterial(approximate_diameter) - machine_manager.setMaterial(position, material_node) + machine_manager.setMaterial(str(position), material_node) self.forceUpdate() diff --git a/cura/UI/ObjectsModel.py b/cura/UI/ObjectsModel.py index 659732e895..1383476665 100644 --- a/cura/UI/ObjectsModel.py +++ b/cura/UI/ObjectsModel.py @@ -31,8 +31,9 @@ class _NodeInfo: self.is_group = is_group # type: bool -## Keep track of all objects in the project class ObjectsModel(ListModel): + """Keep track of all objects in the project""" + NameRole = Qt.UserRole + 1 SelectedRole = Qt.UserRole + 2 OutsideAreaRole = Qt.UserRole + 3 diff --git a/cura/UI/PrintInformation.py b/cura/UI/PrintInformation.py index c39314dc02..ae4aab0407 100644 --- a/cura/UI/PrintInformation.py +++ b/cura/UI/PrintInformation.py @@ -21,11 +21,13 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## A class for processing and the print times per build plate as well as managing the job name -# -# This class also mangles the current machine name and the filename of the first loaded mesh into a job name. -# This job name is requested by the JobSpecs qml file. class PrintInformation(QObject): + """A class for processing and the print times per build plate as well as managing the job name + + This class also mangles the current machine name and the filename of the first loaded mesh into a job name. + This job name is requested by the JobSpecs qml file. + """ + UNTITLED_JOB_NAME = "Untitled" @@ -380,10 +382,12 @@ class PrintInformation(QObject): def baseName(self): return self._base_name - ## Created an acronym-like abbreviated machine name from the currently - # active machine name. - # Called each time the global stack is switched. def _defineAbbreviatedMachineName(self) -> None: + """Created an acronym-like abbreviated machine name from the currently active machine name. + + Called each time the global stack is switched. + """ + global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: self._abbr_machine = "" @@ -392,8 +396,9 @@ class PrintInformation(QObject): self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name) - ## Utility method that strips accents from characters (eg: â -> a) def _stripAccents(self, to_strip: str) -> str: + """Utility method that strips accents from characters (eg: â -> a)""" + return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn') @pyqtSlot(result = "QVariantMap") @@ -431,6 +436,7 @@ class PrintInformation(QObject): return self._change_timer.start() - ## Listen to scene changes to check if we need to reset the print information def _onSceneChanged(self) -> None: + """Listen to scene changes to check if we need to reset the print information""" + self.setToZeroPrintInformation(self._active_build_plate) diff --git a/cura/UltimakerCloud/UltimakerCloudAuthentication.py b/cura/UltimakerCloud/UltimakerCloudConstants.py similarity index 68% rename from cura/UltimakerCloud/UltimakerCloudAuthentication.py rename to cura/UltimakerCloud/UltimakerCloudConstants.py index c8346e5c4e..0c8ea0c9c7 100644 --- a/cura/UltimakerCloud/UltimakerCloudAuthentication.py +++ b/cura/UltimakerCloud/UltimakerCloudConstants.py @@ -7,6 +7,11 @@ DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str DEFAULT_CLOUD_API_VERSION = "1" # type: str DEFAULT_CLOUD_ACCOUNT_API_ROOT = "https://account.ultimaker.com" # type: str +DEFAULT_DIGITAL_FACTORY_URL = "https://digitalfactory.ultimaker.com" # type: str + +# Container Metadata keys +META_UM_LINKED_TO_ACCOUNT = "um_linked_to_account" +"""(bool) Whether a cloud printer is linked to an Ultimaker account""" try: from cura.CuraVersion import CuraCloudAPIRoot # type: ignore @@ -28,3 +33,10 @@ try: CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT except ImportError: CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT + +try: + from cura.CuraVersion import CuraDigitalFactoryURL # type: ignore + if CuraDigitalFactoryURL == "": + CuraDigitalFactoryURL = DEFAULT_DIGITAL_FACTORY_URL +except ImportError: + CuraDigitalFactoryURL = DEFAULT_DIGITAL_FACTORY_URL diff --git a/cura/Utils/Decorators.py b/cura/Utils/Decorators.py index 9275ee6ce9..b478f172bc 100644 --- a/cura/Utils/Decorators.py +++ b/cura/Utils/Decorators.py @@ -11,13 +11,15 @@ from typing import Callable SEMANTIC_VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+(\.[0-9]+)?$") -## Decorator for functions that belong to a set of APIs. For now, this should only be used for officially supported -# APIs, meaning that those APIs should be versioned and maintained. -# -# \param since_version The earliest version since when this API becomes supported. This means that since this version, -# this API function is supposed to behave the same. This parameter is not used. It's just a -# documentation. def api(since_version: str) -> Callable: + """Decorator for functions that belong to a set of APIs. For now, this should only be used for officially supported + + APIs, meaning that those APIs should be versioned and maintained. + + :param since_version: The earliest version since when this API becomes supported. This means that since this version, + this API function is supposed to behave the same. This parameter is not used. It's just a + documentation. + """ # Make sure that APi versions are semantic versions if not SEMANTIC_VERSION_REGEX.fullmatch(since_version): raise ValueError("API since_version [%s] is not a semantic version." % since_version) diff --git a/cura_app.py b/cura_app.py index 6c9839e79c..a78e7cabd1 100755 --- a/cura_app.py +++ b/cura_app.py @@ -63,14 +63,17 @@ if with_sentry_sdk: # Errors to be ignored by Sentry ignore_errors = [KeyboardInterrupt, MemoryError] - sentry_sdk.init("https://5034bf0054fb4b889f82896326e79b13@sentry.io/1821564", - before_send = CrashHandler.sentryBeforeSend, - environment = sentry_env, - release = "cura%s" % ApplicationMetadata.CuraVersion, - default_integrations = False, - max_breadcrumbs = 300, - server_name = "cura", - ignore_errors = ignore_errors) + try: + sentry_sdk.init("https://5034bf0054fb4b889f82896326e79b13@sentry.io/1821564", + before_send = CrashHandler.sentryBeforeSend, + environment = sentry_env, + release = "cura%s" % ApplicationMetadata.CuraVersion, + default_integrations = False, + max_breadcrumbs = 300, + server_name = "cura", + ignore_errors = ignore_errors) + except Exception: + with_sentry_sdk = False if not known_args["debug"]: def get_cura_dir_path(): diff --git a/docker/build.sh b/docker/build.sh index a500663c64..39632b348b 100755 --- a/docker/build.sh +++ b/docker/build.sh @@ -67,4 +67,4 @@ cmake3 \ -DBUILD_TESTS=ON \ .. make -ctest3 --output-on-failure -T Test +ctest3 -j4 --output-on-failure -T Test diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 1ef17458a6..546c6fc27b 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -32,8 +32,9 @@ except ImportError: import xml.etree.ElementTree as ET -## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes! class ThreeMFReader(MeshReader): + """Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!""" + def __init__(self) -> None: super().__init__() @@ -55,13 +56,17 @@ class ThreeMFReader(MeshReader): return Matrix() split_transformation = transformation.split() - ## Transformation is saved as: - ## M00 M01 M02 0.0 - ## M10 M11 M12 0.0 - ## M20 M21 M22 0.0 - ## M30 M31 M32 1.0 - ## We switch the row & cols as that is how everyone else uses matrices! temp_mat = Matrix() + """Transformation is saved as: + M00 M01 M02 0.0 + + M10 M11 M12 0.0 + + M20 M21 M22 0.0 + + M30 M31 M32 1.0 + We switch the row & cols as that is how everyone else uses matrices! + """ # Rotation & Scale temp_mat._data[0, 0] = split_transformation[0] temp_mat._data[1, 0] = split_transformation[1] @@ -80,9 +85,11 @@ class ThreeMFReader(MeshReader): return temp_mat - ## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node. - # \returns Scene node. def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]: + """Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node. + + :returns: Scene node. + """ try: node_name = savitar_node.getName() node_id = savitar_node.getId() @@ -243,15 +250,17 @@ class ThreeMFReader(MeshReader): return result - ## Create a scale vector based on a unit string. - # The core spec defines the following: - # * micron - # * millimeter (default) - # * centimeter - # * inch - # * foot - # * meter def _getScaleFromUnit(self, unit: Optional[str]) -> Vector: + """Create a scale vector based on a unit string. + + .. The core spec defines the following: + * micron + * millimeter (default) + * centimeter + * inch + * foot + * meter + """ conversion_to_mm = { "micron": 0.001, "millimeter": 1, diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 74589b8335..eaac225e00 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -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. from configparser import ConfigParser @@ -89,8 +89,9 @@ class ExtruderInfo: self.intent_info = None -## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): + """Base implementation for reading 3MF workspace files.""" + def __init__(self) -> None: super().__init__() @@ -130,18 +131,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._old_new_materials = {} self._machine_info = None - ## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results. - # This has nothing to do with speed, but with getting consistent new naming for instances & objects. def getNewId(self, old_id: str): + """Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results. + + This has nothing to do with speed, but with getting consistent new naming for instances & objects. + """ if old_id not in self._id_mapping: self._id_mapping[old_id] = self._container_registry.uniqueName(old_id) return self._id_mapping[old_id] - ## Separates the given file list into a list of GlobalStack files and a list of ExtruderStack files. - # - # In old versions, extruder stack files have the same suffix as container stack files ".stack.cfg". - # def _determineGlobalAndExtruderStackFiles(self, project_file_name: str, file_list: List[str]) -> Tuple[str, List[str]]: + """Separates the given file list into a list of GlobalStack files and a list of ExtruderStack files. + + In old versions, extruder stack files have the same suffix as container stack files ".stack.cfg". + """ + archive = zipfile.ZipFile(project_file_name, "r") global_stack_file_list = [name for name in file_list if name.endswith(self._global_stack_suffix)] @@ -181,10 +185,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return global_stack_file_list[0], extruder_stack_file_list - ## read some info so we can make decisions - # \param file_name - # \param show_dialog In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog. def preRead(self, file_name, show_dialog=True, *args, **kwargs): + """Read some info so we can make decisions + + :param file_name: + :param show_dialog: In case we use preRead() to check if a file is a valid project file, + we don't want to show a dialog. + """ self._clearState() self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) @@ -361,15 +368,20 @@ class ThreeMFWorkspaceReader(WorkspaceReader): machine_name = self._getMachineNameFromSerializedStack(serialized) self._machine_info.metadata_dict = self._getMetaDataDictFromSerializedStack(serialized) + # Check if the definition has been changed (this usually happens due to an upgrade) + id_list = self._getContainerIdListFromSerialized(serialized) + if id_list[7] != machine_definition_id: + machine_definition_id = id_list[7] + stacks = self._container_registry.findContainerStacks(name = machine_name, type = "machine") self._is_same_machine_type = True existing_global_stack = None + if stacks: global_stack = stacks[0] existing_global_stack = global_stack containers_found_dict["machine"] = True # Check if there are any changes at all in any of the container stacks. - id_list = self._getContainerIdListFromSerialized(serialized) for index, container_id in enumerate(id_list): # take into account the old empty container IDs container_id = self._old_empty_profile_id_dict.get(container_id, container_id) @@ -459,10 +471,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): extruder_info.intent_info = instance_container_info_dict[intent_id] if not machine_conflict and containers_found_dict["machine"]: - if position not in global_stack.extruders: + if int(position) >= len(global_stack.extrurderList): continue - existing_extruder_stack = global_stack.extruders[position] + existing_extruder_stack = global_stack.extruderList[int(position)] # check if there are any changes at all in any of the container stacks. id_list = self._getContainerIdListFromSerialized(serialized) for index, container_id in enumerate(id_list): @@ -578,18 +590,28 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return WorkspaceReader.PreReadResult.accepted - ## Read the project file - # Add all the definitions / materials / quality changes that do not exist yet. Then it loads - # all the stacks into the container registry. In some cases it will reuse the container for the global stack. - # It handles old style project files containing .stack.cfg as well as new style project files - # containing global.cfg / extruder.cfg - # - # \param file_name @call_on_qt_thread def read(self, file_name): + """Read the project file + + Add all the definitions / materials / quality changes that do not exist yet. Then it loads + all the stacks into the container registry. In some cases it will reuse the container for the global stack. + It handles old style project files containing .stack.cfg as well as new style project files + containing global.cfg / extruder.cfg + + :param file_name: + """ application = CuraApplication.getInstance() - archive = zipfile.ZipFile(file_name, "r") + try: + archive = zipfile.ZipFile(file_name, "r") + except EnvironmentError as e: + message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags or !", + "Project file {0} is suddenly inaccessible: {1}.", file_name, str(e)), + title = i18n_catalog.i18nc("@info:title", "Can't Open Project File")) + message.show() + self.setWorkspaceName("") + return [], {} cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")] @@ -623,13 +645,19 @@ class ThreeMFWorkspaceReader(WorkspaceReader): machine_name = self._container_registry.uniqueName(self._machine_info.name) global_stack = CuraStackBuilder.createMachine(machine_name, self._machine_info.definition_id) - if global_stack: #Only switch if creating the machine was successful. - extruder_stack_dict = global_stack.extruders + if global_stack: # Only switch if creating the machine was successful. + extruder_stack_dict = {str(position): extruder for position, extruder in enumerate(global_stack.extruderList)} self._container_registry.addContainer(global_stack) else: # Find the machine - global_stack = self._container_registry.findContainerStacks(name = self._machine_info.name, type = "machine")[0] + global_stacks = self._container_registry.findContainerStacks(name = self._machine_info.name, type = "machine") + if not global_stacks: + message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tag !", "Project file {0} is made using profiles that are unknown to this version of Ultimaker Cura.", file_name)) + message.show() + self.setWorkspaceName("") + return [], {} + global_stack = global_stacks[0] extruder_stacks = self._container_registry.findContainerStacks(machine = global_stack.getId(), type = "extruder_train") extruder_stack_dict = {stack.getMetaDataEntry("position"): stack for stack in extruder_stacks} @@ -644,6 +672,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): definition_container_files = [name for name in cura_file_names if name.endswith(self._definition_container_suffix)] for definition_container_file in definition_container_files: container_id = self._stripFileToId(definition_container_file) + definitions = self._container_registry.findDefinitionContainersMetadata(id = container_id) if not definitions: definition_container = DefinitionContainer(container_id) @@ -790,7 +819,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): extruder_stack = None intent_category = None # type: Optional[str] if position is not None: - extruder_stack = global_stack.extruders[position] + extruder_stack = global_stack.extruderList[int(position)] intent_category = quality_changes_intent_category_per_extruder[position] container = self._createNewQualityChanges(quality_changes_quality_type, intent_category, quality_changes_name, global_stack, extruder_stack) container_info.container = container @@ -818,9 +847,9 @@ class ThreeMFWorkspaceReader(WorkspaceReader): quality_changes_info.extruder_info_dict["0"] = container_info # If the global stack we're "targeting" has never been active, but was updated from Cura 3.4, # it might not have its extruders set properly. - if not global_stack.extruders: + if len(global_stack.extruderList) == 0: ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack) - extruder_stack = global_stack.extruders["0"] + extruder_stack = global_stack.extruderList[0] intent_category = quality_changes_intent_category_per_extruder["0"] container = self._createNewQualityChanges(quality_changes_quality_type, intent_category, quality_changes_name, global_stack, extruder_stack) @@ -849,7 +878,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): continue if container_info.container is None: - extruder_stack = global_stack.extruders[position] + extruder_stack = global_stack.extruderList[int(position)] intent_category = quality_changes_intent_category_per_extruder[position] container = self._createNewQualityChanges(quality_changes_quality_type, intent_category, quality_changes_name, global_stack, extruder_stack) container_info.container = container @@ -860,19 +889,22 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._machine_info.quality_changes_info.name = quality_changes_name - ## Helper class to create a new quality changes profile. - # - # This will then later be filled with the appropriate data. - # \param quality_type The quality type of the new profile. - # \param intent_category The intent category of the new profile. - # \param name The name for the profile. This will later be made unique so - # it doesn't need to be unique yet. - # \param global_stack The global stack showing the configuration that the - # profile should be created for. - # \param extruder_stack The extruder stack showing the configuration that - # the profile should be created for. If this is None, it will be created - # for the global stack. def _createNewQualityChanges(self, quality_type: str, intent_category: Optional[str], name: str, global_stack: GlobalStack, extruder_stack: Optional[ExtruderStack]) -> InstanceContainer: + """Helper class to create a new quality changes profile. + + This will then later be filled with the appropriate data. + + :param quality_type: The quality type of the new profile. + :param intent_category: The intent category of the new profile. + :param name: The name for the profile. This will later be made unique so + it doesn't need to be unique yet. + :param global_stack: The global stack showing the configuration that the + profile should be created for. + :param extruder_stack: The extruder stack showing the configuration that + the profile should be created for. If this is None, it will be created + for the global stack. + """ + container_registry = CuraApplication.getInstance().getContainerRegistry() base_id = global_stack.definition.getId() if extruder_stack is None else extruder_stack.getId() new_id = base_id + "_" + name @@ -1081,9 +1113,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): def _getXmlProfileClass(self): return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile")) - ## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data. @staticmethod def _getContainerIdListFromSerialized(serialized): + """Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.""" + parser = ConfigParser(interpolation = None, empty_lines_in_values = False) parser.read_string(serialized) diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index 3df7f1f570..3c97146583 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -229,9 +229,10 @@ class WorkspaceDialog(QObject): if key in self._result: self._result[key] = strategy - ## Close the backend: otherwise one could end up with "Slicing..." @pyqtSlot() def closeBackend(self): + """Close the backend: otherwise one could end up with "Slicing...""" + Application.getInstance().getBackend().close() def setMaterialConflict(self, material_conflict): @@ -283,8 +284,9 @@ class WorkspaceDialog(QObject): self.showDialogSignal.emit() @pyqtSlot() - ## Used to notify the dialog so the lock can be released. def notifyClosed(self): + """Used to notify the dialog so the lock can be released.""" + self._result = {} # The result should be cleared before hide, because after it is released the main thread lock self._visible = False try: @@ -319,8 +321,9 @@ class WorkspaceDialog(QObject): self._view.hide() self.hide() - ## Block thread until the dialog is closed. def waitForClose(self): + """Block thread until the dialog is closed.""" + if self._visible: if threading.current_thread() != threading.main_thread(): self._lock.acquire() diff --git a/plugins/3MFReader/__init__.py b/plugins/3MFReader/__init__.py index d68338c35f..5e2b68fce0 100644 --- a/plugins/3MFReader/__init__.py +++ b/plugins/3MFReader/__init__.py @@ -33,7 +33,7 @@ def getMetaData() -> Dict: "description": catalog.i18nc("@item:inlistbox", "3MF File") } ] - + return metaData diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 4b6b978342..4201573c78 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -51,7 +51,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): self._writeContainerToArchive(container, archive) # Check if the machine has extruders and save all that data as well. - for extruder_stack in global_stack.extruders.values(): + for extruder_stack in global_stack.extruderList: self._writeContainerToArchive(extruder_stack, archive) for container in extruder_stack.getContainers(): self._writeContainerToArchive(container, archive) @@ -107,11 +107,13 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): 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. - # \param container That follows the \type{ContainerInterface} to archive. - # \param archive The archive to write to. @staticmethod def _writeContainerToArchive(container, archive): + """Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive. + + :param container: That follows the :type{ContainerInterface} to archive. + :param archive: The archive to write to. + """ if isinstance(container, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())): return # Empty file, do nothing. diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 05dc26f9ad..6c02935080 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -60,15 +60,19 @@ class ThreeMFWriter(MeshWriter): result += str(matrix._data[2, 3]) return result - ## Should we store the archive - # Note that if this is true, the archive will not be closed. - # The object that set this parameter is then responsible for closing it correctly! def setStoreArchive(self, store_archive): + """Should we store the archive + + Note that if this is true, the archive will not be closed. + The object that set this parameter is then responsible for closing it correctly! + """ self._store_archive = store_archive - ## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode - # \returns Uranium Scene node. def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()): + """Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode + + :returns: Uranium Scene node. + """ if not isinstance(um_node, SceneNode): return None diff --git a/plugins/AMFReader/AMFReader.py b/plugins/AMFReader/AMFReader.py index 794f2798ec..ef785f2f53 100644 --- a/plugins/AMFReader/AMFReader.py +++ b/plugins/AMFReader/AMFReader.py @@ -147,13 +147,13 @@ class AMFReader(MeshReader): return group_node - ## Converts a Trimesh to Uranium's MeshData. - # \param tri_node A Trimesh containing the contents of a file that was - # just read. - # \param file_name The full original filename used to watch for changes - # \return Mesh data from the Trimesh in a way that Uranium can understand - # it. def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData: + """Converts a Trimesh to Uranium's MeshData. + + :param tri_node: A Trimesh containing the contents of a file that was just read. + :param file_name: The full original filename used to watch for changes + :return: Mesh data from the Trimesh in a way that Uranium can understand it. + """ tri_faces = tri_node.faces tri_vertices = tri_node.vertices diff --git a/plugins/CuraDrive/src/Settings.py b/plugins/CuraDrive/src/Settings.py index 639c63b45f..56158922dc 100644 --- a/plugins/CuraDrive/src/Settings.py +++ b/plugins/CuraDrive/src/Settings.py @@ -1,13 +1,13 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from cura.UltimakerCloud import UltimakerCloudAuthentication +from cura.UltimakerCloud import UltimakerCloudConstants class Settings: # Keeps the plugin settings. DRIVE_API_VERSION = 1 - DRIVE_API_URL = "{}/cura-drive/v{}".format(UltimakerCloudAuthentication.CuraCloudAPIRoot, str(DRIVE_API_VERSION)) + DRIVE_API_URL = "{}/cura-drive/v{}".format(UltimakerCloudConstants.CuraCloudAPIRoot, str(DRIVE_API_VERSION)) AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled" AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date" diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 0952315235..519d302618 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -42,12 +42,14 @@ catalog = i18nCatalog("cura") class CuraEngineBackend(QObject, Backend): backendError = Signal() - ## Starts the back-end plug-in. - # - # This registers all the signal listeners and prepares for communication - # with the back-end in general. - # CuraEngineBackend is exposed to qml as well. def __init__(self) -> None: + """Starts the back-end plug-in. + + This registers all the signal listeners and prepares for communication + with the back-end in general. + CuraEngineBackend is exposed to qml as well. + """ + super().__init__() # Find out where the engine is located, and how it is called. # This depends on how Cura is packaged and which OS we are running on. @@ -177,18 +179,22 @@ class CuraEngineBackend(QObject, Backend): self._machine_error_checker = self._application.getMachineErrorChecker() self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished) - ## Terminate the engine process. - # - # This function should terminate the engine process. - # Called when closing the application. def close(self) -> None: + """Terminate the engine process. + + This function should terminate the engine process. + Called when closing the application. + """ + # Terminate CuraEngine if it is still running at this point self._terminate() - ## Get the command that is used to call the engine. - # This is useful for debugging and used to actually start the engine. - # \return list of commands and args / parameters. def getEngineCommand(self) -> List[str]: + """Get the command that is used to call the engine. + + 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), ""] parser = argparse.ArgumentParser(prog = "cura", add_help = False) @@ -199,17 +205,18 @@ class CuraEngineBackend(QObject, Backend): return command - ## Emitted when we get a message containing print duration and material amount. - # This also implies the slicing has finished. - # \param time The amount of time the print will take. - # \param material_amount The amount of material the print will use. printDurationMessage = Signal() + """Emitted when we get a message containing print duration and material amount. - ## Emitted when the slicing process starts. + This also implies the slicing has finished. + :param time: The amount of time the print will take. + :param material_amount: The amount of material the print will use. + """ slicingStarted = Signal() + """Emitted when the slicing process starts.""" - ## Emitted when the slicing process is aborted forcefully. slicingCancelled = Signal() + """Emitted when the slicing process is aborted forcefully.""" @pyqtSlot() def stopSlicing(self) -> None: @@ -226,14 +233,16 @@ class CuraEngineBackend(QObject, Backend): if self._error_message: self._error_message.hide() - ## Manually triggers a reslice @pyqtSlot() def forceSlice(self) -> None: + """Manually triggers a reslice""" + self.markSliceAll() self.slice() - ## Perform a slice of the scene. def slice(self) -> None: + """Perform a slice of the scene.""" + Logger.log("i", "Starting to slice...") self._slice_start_time = time() if not self._build_plates_to_be_sliced: @@ -289,9 +298,11 @@ class CuraEngineBackend(QObject, Backend): self._start_slice_job.start() self._start_slice_job.finished.connect(self._onStartSliceCompleted) - ## Terminate the engine process. - # Start the engine process by calling _createSocket() def _terminate(self) -> None: + """Terminate the engine process. + + Start the engine process by calling _createSocket() + """ self._slicing = False self._stored_layer_data = [] if self._start_slice_job_build_plate in self._stored_optimized_layer_data: @@ -316,15 +327,17 @@ class CuraEngineBackend(QObject, Backend): except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this. Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e)) - ## Event handler to call when the job to initiate the slicing process is - # completed. - # - # When the start slice job is successfully completed, it will be happily - # slicing. This function handles any errors that may occur during the - # bootstrapping of a slice job. - # - # \param job The start slice job that was just finished. def _onStartSliceCompleted(self, job: StartSliceJob) -> None: + """Event handler to call when the job to initiate the slicing process is + + completed. + + When the start slice job is successfully completed, it will be happily + slicing. This function handles any errors that may occur during the + bootstrapping of a slice job. + + :param job: The start slice job that was just finished. + """ if self._error_message: self._error_message.hide() @@ -443,11 +456,13 @@ class CuraEngineBackend(QObject, Backend): if self._slice_start_time: Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time ) - ## Determine enable or disable auto slicing. Return True for enable timer and False otherwise. - # It disables when - # - preference auto slice is off - # - decorator isBlockSlicing is found (used in g-code reader) def determineAutoSlicing(self) -> bool: + """Determine enable or disable auto slicing. Return True for enable timer and False otherwise. + + It disables when: + - preference auto slice is off + - decorator isBlockSlicing is found (used in g-code reader) + """ enable_timer = True self._is_disabled = False @@ -472,8 +487,9 @@ class CuraEngineBackend(QObject, Backend): self.disableTimer() return False - ## Return a dict with number of objects per build plate def _numObjectsPerBuildPlate(self) -> Dict[int, int]: + """Return a dict with number of objects per build plate""" + num_objects = defaultdict(int) #type: Dict[int, int] for node in DepthFirstIterator(self._scene.getRoot()): # Only count sliceable objects @@ -483,12 +499,14 @@ class CuraEngineBackend(QObject, Backend): num_objects[build_plate_number] += 1 return num_objects - ## Listener for when the scene has changed. - # - # This should start a slice if the scene is now ready to slice. - # - # \param source The scene node that was changed. def _onSceneChanged(self, source: SceneNode) -> None: + """Listener for when the scene has changed. + + This should start a slice if the scene is now ready to slice. + + :param source: The scene node that was changed. + """ + if not source.callDecoration("isSliceable") and source != self._scene.getRoot(): return @@ -536,10 +554,12 @@ class CuraEngineBackend(QObject, Backend): self._invokeSlice() - ## Called when an error occurs in the socket connection towards the engine. - # - # \param error The exception that occurred. def _onSocketError(self, error: Arcus.Error) -> None: + """Called when an error occurs in the socket connection towards the engine. + + :param error: The exception that occurred. + """ + if self._application.isShuttingDown(): return @@ -567,8 +587,9 @@ class CuraEngineBackend(QObject, Backend): break return has_slicable - ## Remove old layer data (if any) def _clearLayerData(self, build_plate_numbers: Set = None) -> None: + """Remove old layer data (if any)""" + # Clear out any old gcode self._scene.gcode_dict = {} # type: ignore @@ -583,8 +604,9 @@ class CuraEngineBackend(QObject, Backend): if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) - ## Convenient function: mark everything to slice, emit state and clear layer data def needsSlicing(self) -> None: + """Convenient function: mark everything to slice, emit state and clear layer data""" + # CURA-6604: If there's no slicable object, do not (try to) trigger slice, which will clear all the current # gcode. This can break Gcode file loading if it tries to remove it afterwards. if not self.hasSlicableObject(): @@ -597,10 +619,12 @@ class CuraEngineBackend(QObject, Backend): # With manually having to slice, we want to clear the old invalid layer data. self._clearLayerData() - ## A setting has changed, so check if we must reslice. - # \param instance The setting instance that has changed. - # \param property The property of the setting instance that has changed. def _onSettingChanged(self, instance: SettingInstance, property: str) -> None: + """A setting has changed, so check if we must reslice. + + :param instance: The setting instance that has changed. + :param property: The property of the setting instance that has changed. + """ if property == "value": # Only reslice if the value has changed. self.needsSlicing() self._onChanged() @@ -618,25 +642,31 @@ class CuraEngineBackend(QObject, Backend): self.needsSlicing() self._onChanged() - ## Called when a sliced layer data message is received from the engine. - # - # \param message The protobuf message containing sliced layer data. def _onLayerMessage(self, message: Arcus.PythonMessage) -> None: + """Called when a sliced layer data message is received from the engine. + + :param message: The protobuf message containing sliced layer data. + """ + self._stored_layer_data.append(message) - ## Called when an optimized sliced layer data message is received from the engine. - # - # \param message The protobuf message containing sliced layer data. def _onOptimizedLayerMessage(self, message: Arcus.PythonMessage) -> None: + """Called when an optimized sliced layer data message is received from the engine. + + :param message: The protobuf message containing sliced layer data. + """ + if self._start_slice_job_build_plate is not None: if self._start_slice_job_build_plate not in self._stored_optimized_layer_data: self._stored_optimized_layer_data[self._start_slice_job_build_plate] = [] self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message) - ## Called when a progress message is received from the engine. - # - # \param message The protobuf message containing the slicing progress. def _onProgressMessage(self, message: Arcus.PythonMessage) -> None: + """Called when a progress message is received from the engine. + + :param message: The protobuf message containing the slicing progress. + """ + self.processingProgress.emit(message.amount) self.setState(BackendState.Processing) @@ -653,10 +683,12 @@ class CuraEngineBackend(QObject, Backend): else: self._change_timer.start() - ## Called when the engine sends a message that slicing is finished. - # - # \param message The protobuf message signalling that slicing is finished. def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None: + """Called when the engine sends a message that slicing is finished. + + :param message: The protobuf message signalling that slicing is finished. + """ + self.setState(BackendState.Done) self.processingProgress.emit(1.0) @@ -698,27 +730,32 @@ class CuraEngineBackend(QObject, Backend): self.enableTimer() # manually enable timer to be able to invoke slice, also when in manual slice mode self._invokeSlice() - ## Called when a g-code message is received from the engine. - # - # \param message The protobuf message containing g-code, encoded as UTF-8. def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None: + """Called when a g-code message is received from the engine. + + :param message: The protobuf message containing g-code, encoded as UTF-8. + """ + try: self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) #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. pass # Throw the message away. - ## Called when a g-code prefix message is received from the engine. - # - # \param message The protobuf message containing the g-code prefix, - # encoded as UTF-8. def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None: + """Called when a g-code prefix message is received from the engine. + + :param message: The protobuf message containing the g-code prefix, + encoded as UTF-8. + """ + try: self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) #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. pass # Throw the message away. - ## Creates a new socket connection. def _createSocket(self, protocol_file: str = None) -> None: + """Creates a new socket connection.""" + if not protocol_file: if not self.getPluginId(): Logger.error("Can't create socket before CuraEngineBackend plug-in is registered.") @@ -731,10 +768,12 @@ class CuraEngineBackend(QObject, Backend): super()._createSocket(protocol_file) self._engine_is_fresh = True - ## Called when anything has changed to the stuff that needs to be sliced. - # - # This indicates that we should probably re-slice soon. def _onChanged(self, *args: Any, **kwargs: Any) -> None: + """Called when anything has changed to the stuff that needs to be sliced. + + This indicates that we should probably re-slice soon. + """ + self.needsSlicing() if self._use_timer: # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, @@ -748,11 +787,13 @@ class CuraEngineBackend(QObject, Backend): else: self._change_timer.start() - ## Called when a print time message is received from the engine. - # - # \param message The protobuf message containing the print time per feature and - # material amount per extruder def _onPrintTimeMaterialEstimates(self, message: Arcus.PythonMessage) -> None: + """Called when a print time message is received from the engine. + + :param message: The protobuf message containing the print time per feature and + material amount per extruder + """ + material_amounts = [] for index in range(message.repeatedMessageCount("materialEstimates")): material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount) @@ -760,10 +801,12 @@ class CuraEngineBackend(QObject, Backend): times = self._parseMessagePrintTimes(message) self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts) - ## Called for parsing message to retrieve estimated time per feature - # - # \param message The protobuf message containing the print time per feature def _parseMessagePrintTimes(self, message: Arcus.PythonMessage) -> Dict[str, float]: + """Called for parsing message to retrieve estimated time per feature + + :param message: The protobuf message containing the print time per feature + """ + result = { "inset_0": message.time_inset_0, "inset_x": message.time_inset_x, @@ -780,19 +823,22 @@ class CuraEngineBackend(QObject, Backend): } return result - ## Called when the back-end connects to the front-end. def _onBackendConnected(self) -> None: + """Called when the back-end connects to the front-end.""" + if self._restart: self._restart = False self._onChanged() - ## Called when the user starts using some tool. - # - # When the user starts using a tool, we should pause slicing to prevent - # continuously slicing while the user is dragging some tool handle. - # - # \param tool The tool that the user is using. def _onToolOperationStarted(self, tool: Tool) -> None: + """Called when the user starts using some tool. + + When the user starts using a tool, we should pause slicing to prevent + continuously slicing while the user is dragging some tool handle. + + :param tool: The tool that the user is using. + """ + self._tool_active = True # Do not react on scene change self.disableTimer() # Restart engine as soon as possible, we know we want to slice afterwards @@ -800,12 +846,14 @@ class CuraEngineBackend(QObject, Backend): self._terminate() self._createSocket() - ## Called when the user stops using some tool. - # - # This indicates that we can safely start slicing again. - # - # \param tool The tool that the user was using. def _onToolOperationStopped(self, tool: Tool) -> None: + """Called when the user stops using some tool. + + This indicates that we can safely start slicing again. + + :param tool: The tool that the user was using. + """ + self._tool_active = False # React on scene change again self.determineAutoSlicing() # Switch timer on if appropriate # Process all the postponed scene changes @@ -819,8 +867,9 @@ class CuraEngineBackend(QObject, Backend): self._process_layers_job.finished.connect(self._onProcessLayersFinished) self._process_layers_job.start() - ## Called when the user changes the active view mode. def _onActiveViewChanged(self) -> None: + """Called when the user changes the active view mode.""" + view = self._application.getController().getActiveView() if view: active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate @@ -838,17 +887,20 @@ class CuraEngineBackend(QObject, Backend): else: self._layer_view_active = False - ## Called when the back-end self-terminates. - # - # We should reset our state and start listening for new connections. def _onBackendQuit(self) -> None: + """Called when the back-end self-terminates. + + We should reset our state and start listening for new connections. + """ + if not self._restart: if self._process: # type: ignore Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) # type: ignore self._process = None # type: ignore - ## Called when the global container stack changes def _onGlobalStackChanged(self) -> None: + """Called when the global container stack changes""" + if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged) self._global_container_stack.containersChanged.disconnect(self._onChanged) @@ -877,15 +929,18 @@ class CuraEngineBackend(QObject, Backend): Logger.log("d", "See if there is more to slice(2)...") self._invokeSlice() - ## Connect slice function to timer. def enableTimer(self) -> None: + """Connect slice function to timer.""" + if not self._use_timer: self._change_timer.timeout.connect(self.slice) self._use_timer = True - ## Disconnect slice function from timer. - # This means that slicing will not be triggered automatically def disableTimer(self) -> None: + """Disconnect slice function from timer. + + This means that slicing will not be triggered automatically + """ if self._use_timer: self._use_timer = False self._change_timer.timeout.disconnect(self.slice) @@ -897,8 +952,9 @@ class CuraEngineBackend(QObject, Backend): if auto_slice: self._change_timer.start() - ## Tickle the backend so in case of auto slicing, it starts the timer. def tickle(self) -> None: + """Tickle the backend so in case of auto slicing, it starts the timer.""" + if self._use_timer: self._change_timer.start() diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 32d60eb68b..e0a20177b5 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -28,10 +28,12 @@ from cura.Machines.Models.ExtrudersModel import ExtrudersModel catalog = i18nCatalog("cura") -## Return a 4-tuple with floats 0-1 representing the html color code -# -# \param color_code html color code, i.e. "#FF0000" -> red def colorCodeToRGBA(color_code): + """Return a 4-tuple with floats 0-1 representing the html color code + + :param color_code: html color code, i.e. "#FF0000" -> red + """ + if color_code is None: Logger.log("w", "Unable to convert color code, returning default") return [0, 0, 0, 1] @@ -51,13 +53,15 @@ class ProcessSlicedLayersJob(Job): self._abort_requested = False self._build_plate_number = None - ## Aborts the processing of layers. - # - # This abort is made on a best-effort basis, meaning that the actual - # job thread will check once in a while to see whether an abort is - # requested and then stop processing by itself. There is no guarantee - # that the abort will stop the job any time soon or even at all. def abort(self): + """Aborts the processing of layers. + + This abort is made on a best-effort basis, meaning that the actual + job thread will check once in a while to see whether an abort is + requested and then stop processing by itself. There is no guarantee + that the abort will stop the job any time soon or even at all. + """ + self._abort_requested = True def setBuildPlate(self, new_value): diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index 62b0bd16e7..693b129f43 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -42,8 +42,9 @@ class StartJobResult(IntEnum): ObjectsWithDisabledExtruder = 8 -## Formatter class that handles token expansion in start/end gcode class GcodeStartEndFormatter(Formatter): + """Formatter class that handles token expansion in start/end gcode""" + def __init__(self, default_extruder_nr: int = -1) -> None: super().__init__() self._default_extruder_nr = default_extruder_nr @@ -84,8 +85,9 @@ class GcodeStartEndFormatter(Formatter): return value -## Job class that builds up the message of scene data to send to CuraEngine. class StartSliceJob(Job): + """Job class that builds up the message of scene data to send to CuraEngine.""" + def __init__(self, slice_message: Arcus.PythonMessage) -> None: super().__init__() @@ -102,9 +104,10 @@ class StartSliceJob(Job): def setBuildPlate(self, build_plate_number: int) -> None: self._build_plate_number = build_plate_number - ## Check if a stack has any errors. - ## returns true if it has errors, false otherwise. def _checkStackForErrors(self, stack: ContainerStack) -> bool: + """Check if a stack has any errors.""" + + """returns true if it has errors, false otherwise.""" top_of_stack = cast(InstanceContainer, stack.getTop()) # Cache for efficiency. changed_setting_keys = top_of_stack.getAllKeys() @@ -134,8 +137,9 @@ class StartSliceJob(Job): return False - ## Runs the job that initiates the slicing. def run(self) -> None: + """Runs the job that initiates the slicing.""" + if self._build_plate_number is None: self.setResult(StartJobResult.Error) return @@ -251,7 +255,7 @@ class StartSliceJob(Job): global_stack = CuraApplication.getInstance().getGlobalContainerStack() if not global_stack: return - extruders_enabled = {position: stack.isEnabled for position, stack in global_stack.extruders.items()} + extruders_enabled = [stack.isEnabled for stack in global_stack.extruderList] filtered_object_groups = [] has_model_with_disabled_extruders = False associated_disabled_extruders = set() @@ -261,7 +265,7 @@ class StartSliceJob(Job): 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") + extruder_position = int(node.callDecoration("getActiveExtruderPosition")) if not is_non_printing_mesh and not extruders_enabled[extruder_position]: skip_group = True has_model_with_disabled_extruders = True @@ -271,8 +275,8 @@ class StartSliceJob(Job): 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)) + associated_disabled_extruders = {p + 1 for p in associated_disabled_extruders} + self.setMessage(", ".join(map(str, sorted(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 @@ -338,14 +342,14 @@ class StartSliceJob(Job): def setIsCancelled(self, value: bool): self._is_cancelled = value - ## Creates a dictionary of tokens to replace in g-code pieces. - # - # This indicates what should be replaced in the start and end g-codes. - # \param stack The stack to get the settings from to replace the tokens - # with. - # \return A dictionary of replacement tokens to the values they should be - # replaced with. def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: + """Creates a dictionary of tokens to replace in g-code pieces. + + This indicates what should be replaced in the start and end g-codes. + :param stack: The stack to get the settings from to replace the tokens with. + :return: A dictionary of replacement tokens to the values they should be replaced with. + """ + result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") @@ -373,10 +377,12 @@ class StartSliceJob(Job): extruder_nr = extruder_stack.getProperty("extruder_nr", "value") self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack) - ## Replace setting tokens in a piece of g-code. - # \param value A piece of g-code to replace tokens in. - # \param default_extruder_nr Stack nr to use when no stack nr is specified, defaults to the global stack def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1) -> str: + """Replace setting tokens in a piece of g-code. + + :param value: A piece of g-code to replace tokens in. + :param default_extruder_nr: Stack nr to use when no stack nr is specified, defaults to the global stack + """ if not self._all_extruders_settings: self._cacheAllExtruderSettings() @@ -392,8 +398,9 @@ class StartSliceJob(Job): Logger.logException("w", "Unable to do token replacement on start/end g-code") return str(value) - ## Create extruder message from stack def _buildExtruderMessage(self, stack: ContainerStack) -> None: + """Create extruder message from stack""" + message = self._slice_message.addRepeatedMessage("extruders") message.id = int(stack.getMetaDataEntry("position")) if not self._all_extruders_settings: @@ -422,11 +429,13 @@ class StartSliceJob(Job): setting.value = str(value).encode("utf-8") Job.yieldThread() - ## Sends all global settings to the engine. - # - # The settings are taken from the global stack. This does not include any - # per-extruder settings or per-object settings. def _buildGlobalSettingsMessage(self, stack: ContainerStack) -> None: + """Sends all global settings to the engine. + + The settings are taken from the global stack. This does not include any + per-extruder settings or per-object settings. + """ + if not self._all_extruders_settings: self._cacheAllExtruderSettings() @@ -460,15 +469,16 @@ class StartSliceJob(Job): setting_message.value = str(value).encode("utf-8") Job.yieldThread() - ## Sends for some settings which extruder they should fallback to if not - # set. - # - # This is only set for settings that have the limit_to_extruder - # property. - # - # \param stack The global stack with all settings, from which to read the - # limit_to_extruder property. def _buildGlobalInheritsStackMessage(self, stack: ContainerStack) -> None: + """Sends for some settings which extruder they should fallback to if not set. + + This is only set for settings that have the limit_to_extruder + property. + + :param stack: The global stack with all settings, from which to read the + limit_to_extruder property. + """ + for key in stack.getAllKeys(): extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder")))) if extruder_position >= 0: # Set to a specific extruder. @@ -477,10 +487,13 @@ class StartSliceJob(Job): setting_extruder.extruder = extruder_position Job.yieldThread() - ## Check if a node has per object settings and ensure that they are set correctly in the message - # \param node Node to check. - # \param message object_lists message to put the per object settings in def _handlePerObjectSettings(self, node: CuraSceneNode, message: Arcus.PythonMessage): + """Check if a node has per object settings and ensure that they are set correctly in the message + + :param node: Node to check. + :param message: object_lists message to put the per object settings in + """ + stack = node.callDecoration("getStack") # Check if the node has a stack attached to it and the stack has any settings in the top container. @@ -516,10 +529,13 @@ class StartSliceJob(Job): Job.yieldThread() - ## Recursive function to put all settings that require each other for value changes in a list - # \param relations_set Set of keys of settings that are influenced - # \param relations list of relation objects that need to be checked. def _addRelations(self, relations_set: Set[str], relations: List[SettingRelation]): + """Recursive function to put all settings that require each other for value changes in a list + + :param relations_set: Set of keys of settings that are influenced + :param relations: list of relation objects that need to be checked. + """ + for relation in filter(lambda r: r.role == "value" or r.role == "limit_to_extruder", relations): if relation.type == RelationType.RequiresTarget: continue diff --git a/plugins/CuraProfileReader/CuraProfileReader.py b/plugins/CuraProfileReader/CuraProfileReader.py index d4e5d393b2..8822e9bb17 100644 --- a/plugins/CuraProfileReader/CuraProfileReader.py +++ b/plugins/CuraProfileReader/CuraProfileReader.py @@ -13,23 +13,30 @@ from cura.ReaderWriters.ProfileReader import ProfileReader import zipfile -## A plugin that reads profile data from Cura profile files. -# -# It reads a profile from a .curaprofile file, and returns it as a profile -# instance. + class CuraProfileReader(ProfileReader): - ## Initialises the cura profile reader. - # This does nothing since the only other function is basically stateless. + """A plugin that reads profile data from Cura profile files. + + It reads a profile from a .curaprofile file, and returns it as a profile + instance. + """ + def __init__(self) -> None: + """Initialises the cura profile reader. + + This does nothing since the only other function is basically stateless. + """ super().__init__() - ## Reads a cura profile from a file and returns it. - # - # \param file_name The file to read the cura profile from. - # \return The cura profiles that were in the file, if any. If the file - # could not be read or didn't contain a valid profile, ``None`` is - # returned. def read(self, file_name: str) -> List[Optional[InstanceContainer]]: + """Reads a cura profile from a file and returns it. + + :param file_name: The file to read the cura profile from. + :return: The cura profiles that were in the file, if any. If the file + could not be read or didn't contain a valid profile, ``None`` is + returned. + """ + try: with zipfile.ZipFile(file_name, "r") as archive: results = [] # type: List[Optional[InstanceContainer]] @@ -50,13 +57,14 @@ class CuraProfileReader(ProfileReader): serialized_bytes = fhandle.read() return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized_bytes, file_name)] - ## Convert a profile from an old Cura to this Cura if needed. - # - # \param serialized The profile data to convert in the serialized on-disk - # format. - # \param profile_id The name of the profile. - # \return List of serialized profile strings and matching profile names. def _upgradeProfile(self, serialized: str, profile_id: str) -> List[Tuple[str, str]]: + """Convert a profile from an old Cura to this Cura if needed. + + :param serialized: The profile data to convert in the serialized on-disk format. + :param profile_id: The name of the profile. + :return: List of serialized profile strings and matching profile names. + """ + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -75,12 +83,14 @@ class CuraProfileReader(ProfileReader): else: return [(serialized, profile_id)] - ## Load a profile from a serialized string. - # - # \param serialized The profile data to read. - # \param profile_id The name of the profile. - # \return The profile that was stored in the string. def _loadProfile(self, serialized: str, profile_id: str) -> Optional[InstanceContainer]: + """Load a profile from a serialized string. + + :param serialized: The profile data to read. + :param profile_id: The name of the profile. + :return: The profile that was stored in the string. + """ + # Create an empty profile. profile = InstanceContainer(profile_id) profile.setMetaDataEntry("type", "quality_changes") @@ -102,13 +112,15 @@ class CuraProfileReader(ProfileReader): profile.setMetaDataEntry("definition", active_quality_definition) return profile - ## Upgrade a serialized profile to the current profile format. - # - # \param serialized The profile data to convert. - # \param profile_id The name of the profile. - # \param source_version The profile version of 'serialized'. - # \return List of serialized profile strings and matching profile names. def _upgradeProfileVersion(self, serialized: str, profile_id: str, main_version: int, setting_version: int) -> List[Tuple[str, str]]: + """Upgrade a serialized profile to the current profile format. + + :param serialized: The profile data to convert. + :param profile_id: The name of the profile. + :param source_version: The profile version of 'serialized'. + :return: List of serialized profile strings and matching profile names. + """ + source_version = main_version * 1000000 + setting_version from UM.VersionUpgradeManager import VersionUpgradeManager diff --git a/plugins/CuraProfileWriter/CuraProfileWriter.py b/plugins/CuraProfileWriter/CuraProfileWriter.py index 78f0b078d9..56f4d07a74 100644 --- a/plugins/CuraProfileWriter/CuraProfileWriter.py +++ b/plugins/CuraProfileWriter/CuraProfileWriter.py @@ -6,15 +6,18 @@ from UM.Logger import Logger from cura.ReaderWriters.ProfileWriter import ProfileWriter import zipfile -## Writes profiles to Cura's own profile format with config files. class CuraProfileWriter(ProfileWriter): - ## Writes a profile to the specified file path. - # - # \param path \type{string} The file to output to. - # \param profiles \type{Profile} \type{List} The profile(s) to write to that file. - # \return \code True \endcode if the writing was successful, or \code - # False \endcode if it wasn't. + """Writes profiles to Cura's own profile format with config files.""" + def write(self, path, profiles): + """Writes a profile to the specified file path. + + :param path: :type{string} The file to output to. + :param profiles: :type{Profile} :type{List} The profile(s) to write to that file. + :return: True if the writing was successful, or + False if it wasn't. + """ + if type(profiles) != list: profiles = [profiles] diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py index 9c4d498d7e..8d0670c844 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py @@ -18,10 +18,12 @@ from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage i18n_catalog = i18nCatalog("cura") -## This Extension checks for new versions of the firmware based on the latest checked version number. -# The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy -# to change it to work for other applications. class FirmwareUpdateChecker(Extension): + """This Extension checks for new versions of the firmware based on the latest checked version number. + + The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy + to change it to work for other applications. + """ def __init__(self) -> None: super().__init__() @@ -35,8 +37,9 @@ class FirmwareUpdateChecker(Extension): self._check_job = None self._checked_printer_names = set() # type: Set[str] - ## Callback for the message that is spawned when there is a new version. def _onActionTriggered(self, message, action): + """Callback for the message that is spawned when there is a new version.""" + if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD: machine_id = message.getMachineId() download_url = message.getDownloadUrl() @@ -57,13 +60,15 @@ class FirmwareUpdateChecker(Extension): def _onJobFinished(self, *args, **kwargs): self._check_job = None - ## Connect with software.ultimaker.com, load latest.version and check version info. - # If the version info is different from the current version, spawn a message to - # allow the user to download it. - # - # \param silent type(boolean) Suppresses messages other than "new version found" messages. - # This is used when checking for a new firmware version at startup. def checkFirmwareVersion(self, container = None, silent = False): + """Connect with software.ultimaker.com, load latest.version and check version info. + + If the version info is different from the current version, spawn a message to + allow the user to download it. + + :param silent: type(boolean) Suppresses messages other than "new version found" messages. + This is used when checking for a new firmware version at startup. + """ container_name = container.definition.getName() if container_name in self._checked_printer_names: return diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py index 279b397777..2c869195bc 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.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. from UM.Application import Application @@ -21,8 +21,9 @@ from UM.i18n import i18nCatalog i18n_catalog = i18nCatalog("cura") -## This job checks if there is an update available on the provided URL. class FirmwareUpdateCheckerJob(Job): + """This job checks if there is an update available on the provided URL.""" + STRING_ZERO_VERSION = "0.0.0" STRING_EPSILON_VERSION = "0.0.1" ZERO_VERSION = Version(STRING_ZERO_VERSION) @@ -113,7 +114,7 @@ class FirmwareUpdateCheckerJob(Job): # notify the user when no new firmware version is available. if (checked_version != "") and (checked_version != current_version): Logger.log("i", "Showing firmware update message for new version: {version}".format(version = current_version)) - message = FirmwareUpdateCheckerMessage(machine_id, self._machine_name, + message = FirmwareUpdateCheckerMessage(machine_id, self._machine_name, current_version, self._lookups.getRedirectUserUrl()) message.actionTriggered.connect(self._callback) message.show() diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerMessage.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerMessage.py index 58c00850cb..ca253e3ec6 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerMessage.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerMessage.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. from UM.i18n import i18nCatalog @@ -11,11 +11,12 @@ i18n_catalog = i18nCatalog("cura") class FirmwareUpdateCheckerMessage(Message): STR_ACTION_DOWNLOAD = "download" - def __init__(self, machine_id: int, machine_name: str, download_url: str) -> None: + def __init__(self, machine_id: int, machine_name: str, latest_version: str, download_url: str) -> None: super().__init__(i18n_catalog.i18nc( "@info Don't translate {machine_name}, since it gets replaced by a printer name!", - "New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format( - machine_name = machine_name), + "New features or bug-fixes may be available for your {machine_name}! If not already at the latest version, " + "it is recommended to update the firmware on your printer to version {latest_version}.").format( + machine_name = machine_name, latest_version = latest_version), title = i18n_catalog.i18nc( "@info:title The %s gets replaced with the printer name.", "New %s firmware available") % machine_name) diff --git a/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py b/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py index e2b0041674..35f338fb04 100644 --- a/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py +++ b/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py @@ -19,8 +19,10 @@ if MYPY: catalog = i18nCatalog("cura") -## Upgrade the firmware of a machine by USB with this action. + class FirmwareUpdaterMachineAction(MachineAction): + """Upgrade the firmware of a machine by USB with this action.""" + def __init__(self) -> None: super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Update Firmware")) self._qml_url = "FirmwareUpdaterMachineAction.qml" diff --git a/plugins/GCodeGzReader/GCodeGzReader.py b/plugins/GCodeGzReader/GCodeGzReader.py index a528b494e9..85a5b01107 100644 --- a/plugins/GCodeGzReader/GCodeGzReader.py +++ b/plugins/GCodeGzReader/GCodeGzReader.py @@ -7,10 +7,13 @@ from UM.Mesh.MeshReader import MeshReader #The class we're extending/implementin from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType #To add the .gcode.gz files to the MIME type database. from UM.PluginRegistry import PluginRegistry -## A file reader that reads gzipped g-code. -# -# If you're zipping g-code, you might as well use gzip! + class GCodeGzReader(MeshReader): + """A file reader that reads gzipped g-code. + + If you're zipping g-code, you might as well use gzip! + """ + def __init__(self) -> None: super().__init__() MimeTypeDatabase.addMimeType( diff --git a/plugins/GCodeGzWriter/GCodeGzWriter.py b/plugins/GCodeGzWriter/GCodeGzWriter.py index cbbfb8f986..2bbaaeb0a3 100644 --- a/plugins/GCodeGzWriter/GCodeGzWriter.py +++ b/plugins/GCodeGzWriter/GCodeGzWriter.py @@ -13,26 +13,31 @@ from UM.Scene.SceneNode import SceneNode #For typing. from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") -## A file writer that writes gzipped g-code. -# -# If you're zipping g-code, you might as well use gzip! + class GCodeGzWriter(MeshWriter): + """A file writer that writes gzipped g-code. + + If you're zipping g-code, you might as well use gzip! + """ + def __init__(self) -> None: super().__init__(add_to_recent_files = False) - ## Writes the gzipped g-code to a stream. - # - # Note that even though the function accepts a collection of nodes, the - # entire scene is always written to the file since it is not possible to - # separate the g-code for just specific nodes. - # - # \param stream The stream to write the gzipped g-code to. - # \param nodes This is ignored. - # \param mode Additional information on what type of stream to use. This - # must always be binary mode. - # \return Whether the write was successful. def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode) -> bool: + """Writes the gzipped g-code to a stream. + + Note that even though the function accepts a collection of nodes, the + entire scene is always written to the file since it is not possible to + separate the g-code for just specific nodes. + + :param stream: The stream to write the gzipped g-code to. + :param nodes: This is ignored. + :param mode: Additional information on what type of stream to use. This + must always be binary mode. + :return: Whether the write was successful. + """ + if mode != MeshWriter.OutputMode.BinaryMode: Logger.log("e", "GCodeGzWriter does not support text mode.") self.setInformation(catalog.i18nc("@error:not supported", "GCodeGzWriter does not support text mode.")) diff --git a/plugins/GCodeProfileReader/GCodeProfileReader.py b/plugins/GCodeProfileReader/GCodeProfileReader.py index 9fbae7b473..047497e611 100644 --- a/plugins/GCodeProfileReader/GCodeProfileReader.py +++ b/plugins/GCodeProfileReader/GCodeProfileReader.py @@ -3,6 +3,7 @@ import re #Regular expressions for parsing escape characters in the settings. import json +from typing import Optional from UM.Settings.ContainerFormatError import ContainerFormatError from UM.Settings.InstanceContainer import InstanceContainer @@ -12,40 +13,48 @@ catalog = i18nCatalog("cura") from cura.ReaderWriters.ProfileReader import ProfileReader, NoProfileException -## A class that reads profile data from g-code files. -# -# It reads the profile data from g-code files and stores it in a new profile. -# This class currently does not process the rest of the g-code in any way. class GCodeProfileReader(ProfileReader): - ## The file format version of the serialized g-code. - # - # It can only read settings with the same version as the version it was - # written with. If the file format is changed in a way that breaks reverse - # compatibility, increment this version number! - version = 3 + """A class that reads profile data from g-code files. + + It reads the profile data from g-code files and stores it in a new profile. + This class currently does not process the rest of the g-code in any way. + """ + + version = 3 + """The file format version of the serialized g-code. + + It can only read settings with the same version as the version it was + written with. If the file format is changed in a way that breaks reverse + compatibility, increment this version number! + """ - ## Dictionary that defines how characters are escaped when embedded in - # g-code. - # - # Note that the keys of this dictionary are regex strings. The values are - # not. escape_characters = { re.escape("\\\\"): "\\", #The escape character. re.escape("\\n"): "\n", #Newlines. They break off the comment. re.escape("\\r"): "\r" #Carriage return. Windows users may need this for visualisation in their editors. } + """Dictionary that defines how characters are escaped when embedded in + + g-code. + + Note that the keys of this dictionary are regex strings. The values are + not. + """ - ## Initialises the g-code reader as a profile reader. def __init__(self): + """Initialises the g-code reader as a profile reader.""" + super().__init__() - ## Reads a g-code file, loading the profile from it. - # - # \param file_name The name of the file to read the profile from. - # \return The profile that was in the specified file, if any. If the - # specified file was no g-code or contained no parsable profile, \code - # None \endcode is returned. def read(self, file_name): + """Reads a g-code file, loading the profile from it. + + :param file_name: The name of the file to read the profile from. + :return: The profile that was in the specified file, if any. If the + specified file was no g-code or contained no parsable profile, + None is returned. + """ + if file_name.split(".")[-1] != "gcode": return None @@ -94,22 +103,28 @@ class GCodeProfileReader(ProfileReader): profiles.append(readQualityProfileFromString(profile_string)) return profiles -## Unescape a string which has been escaped for use in a gcode comment. -# -# \param string The string to unescape. -# \return \type{str} The unscaped string. -def unescapeGcodeComment(string): + +def unescapeGcodeComment(string: str) -> str: + """Unescape a string which has been escaped for use in a gcode comment. + + :param string: The string to unescape. + :return: The unescaped string. + """ + # Un-escape the serialized profile. pattern = re.compile("|".join(GCodeProfileReader.escape_characters.keys())) # Perform the replacement with a regular expression. return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string) -## Read in a profile from a serialized string. -# -# \param profile_string The profile data in serialized form. -# \return \type{Profile} the resulting Profile object or None if it could not be read. -def readQualityProfileFromString(profile_string): + +def readQualityProfileFromString(profile_string) -> Optional[InstanceContainer]: + """Read in a profile from a serialized string. + + :param profile_string: The profile data in serialized form. + :return: The resulting Profile object or None if it could not be read. + """ + # Create an empty profile - the id and name will be changed by the ContainerRegistry profile = InstanceContainer("") try: diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index 7b19fdb160..a49de266c4 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -28,9 +28,8 @@ PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optiona Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])]) -## This parser is intended to interpret the common firmware codes among all the -# different flavors class FlavorParser: + """This parser is intended to interpret the common firmware codes among all the different flavors""" def __init__(self) -> None: CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage) @@ -212,8 +211,9 @@ class FlavorParser: # G0 and G1 should be handled exactly the same. _gCode1 = _gCode0 - ## Home the head. def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: + """Home the head.""" + return self._position( params.x if params.x is not None else position.x, params.y if params.y is not None else position.y, @@ -221,21 +221,26 @@ class FlavorParser: position.f, position.e) - ## Set the absolute positioning def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: + """Set the absolute positioning""" + self._is_absolute_positioning = True self._is_absolute_extrusion = True return position - ## Set the relative positioning def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: + """Set the relative positioning""" + self._is_absolute_positioning = False self._is_absolute_extrusion = False return position - ## Reset the current position to the values specified. - # For example: G92 X10 will set the X to 10 without any physical motion. def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: + """Reset the current position to the values specified. + + For example: G92 X10 will set the X to 10 without any physical motion. + """ + if params.e is not None: # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width self._extrusion_length_offset[self._extruder_number] = position.e[self._extruder_number] - params.e @@ -291,8 +296,9 @@ class FlavorParser: _type_keyword = ";TYPE:" _layer_keyword = ";LAYER:" - ## For showing correct x, y offsets for each extruder def _extruderOffsets(self) -> Dict[int, List[float]]: + """For showing correct x, y offsets for each extruder""" + result = {} for extruder in ExtruderManager.getInstance().getActiveExtruderStacks(): result[int(extruder.getMetaData().get("position", "0"))] = [ @@ -314,7 +320,7 @@ class FlavorParser: if not global_stack: return None - self._filament_diameter = global_stack.extruders[str(self._extruder_number)].getProperty("material_diameter", "value") + self._filament_diameter = global_stack.extruderList[self._extruder_number].getProperty("material_diameter", "value") scene_node = CuraSceneNode() diff --git a/plugins/GCodeReader/RepRapFlavorParser.py b/plugins/GCodeReader/RepRapFlavorParser.py index 2a24d16add..10b7b78587 100644 --- a/plugins/GCodeReader/RepRapFlavorParser.py +++ b/plugins/GCodeReader/RepRapFlavorParser.py @@ -3,8 +3,10 @@ from . import FlavorParser -## This parser is intended to interpret the RepRap Firmware g-code flavor. + class RepRapFlavorParser(FlavorParser.FlavorParser): + """This parser is intended to interpret the RepRap Firmware g-code flavor.""" + def __init__(self): super().__init__() @@ -17,16 +19,20 @@ class RepRapFlavorParser(FlavorParser.FlavorParser): # Set relative extrusion mode self._is_absolute_extrusion = False - ## Set the absolute positioning - # RepRapFlavor code G90 sets position of X, Y, Z to absolute - # For absolute E, M82 is used def _gCode90(self, position, params, path): + """Set the absolute positioning + + RepRapFlavor code G90 sets position of X, Y, Z to absolute + For absolute E, M82 is used + """ self._is_absolute_positioning = True return position - ## Set the relative positioning - # RepRapFlavor code G91 sets position of X, Y, Z to relative - # For relative E, M83 is used def _gCode91(self, position, params, path): + """Set the relative positioning + + RepRapFlavor code G91 sets position of X, Y, Z to relative + For relative E, M83 is used + """ self._is_absolute_positioning = False return position \ No newline at end of file diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index 792b2aff10..bb901bed89 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -14,34 +14,40 @@ from cura.Machines.ContainerTree import ContainerTree from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") -## Writes g-code to a file. -# -# While this poses as a mesh writer, what this really does is take the g-code -# in the entire scene and write it to an output device. Since the g-code of a -# single mesh isn't separable from the rest what with rafts and travel moves -# and all, it doesn't make sense to write just a single mesh. -# -# So this plug-in takes the g-code that is stored in the root of the scene -# node tree, adds a bit of extra information about the profiles and writes -# that to the output device. -class GCodeWriter(MeshWriter): - ## The file format version of the serialised g-code. - # - # It can only read settings with the same version as the version it was - # written with. If the file format is changed in a way that breaks reverse - # compatibility, increment this version number! - version = 3 - ## Dictionary that defines how characters are escaped when embedded in - # g-code. - # - # Note that the keys of this dictionary are regex strings. The values are - # not. +class GCodeWriter(MeshWriter): + """Writes g-code to a file. + + While this poses as a mesh writer, what this really does is take the g-code + in the entire scene and write it to an output device. Since the g-code of a + single mesh isn't separable from the rest what with rafts and travel moves + and all, it doesn't make sense to write just a single mesh. + + So this plug-in takes the g-code that is stored in the root of the scene + node tree, adds a bit of extra information about the profiles and writes + that to the output device. + """ + + version = 3 + """The file format version of the serialised g-code. + + It can only read settings with the same version as the version it was + written with. If the file format is changed in a way that breaks reverse + compatibility, increment this version number! + """ + escape_characters = { re.escape("\\"): "\\\\", # The escape character. re.escape("\n"): "\\n", # Newlines. They break off the comment. re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors. } + """Dictionary that defines how characters are escaped when embedded in + + g-code. + + Note that the keys of this dictionary are regex strings. The values are + not. + """ _setting_keyword = ";SETTING_" @@ -50,17 +56,19 @@ class GCodeWriter(MeshWriter): self._application = Application.getInstance() - ## Writes the g-code for the entire scene to a stream. - # - # Note that even though the function accepts a collection of nodes, the - # entire scene is always written to the file since it is not possible to - # separate the g-code for just specific nodes. - # - # \param stream The stream to write the g-code to. - # \param nodes This is ignored. - # \param mode Additional information on how to format the g-code in the - # file. This must always be text mode. def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode): + """Writes the g-code for the entire scene to a stream. + + Note that even though the function accepts a collection of nodes, the + entire scene is always written to the file since it is not possible to + separate the g-code for just specific nodes. + + :param stream: The stream to write the g-code to. + :param nodes: This is ignored. + :param mode: Additional information on how to format the g-code in the + file. This must always be text mode. + """ + if mode != MeshWriter.OutputMode.TextMode: Logger.log("e", "GCodeWriter does not support non-text mode.") self.setInformation(catalog.i18nc("@error:not supported", "GCodeWriter does not support non-text mode.")) @@ -88,8 +96,9 @@ class GCodeWriter(MeshWriter): self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting.")) return False - ## Create a new container with container 2 as base and container 1 written over it. def _createFlattenedContainerInstance(self, instance_container1, instance_container2): + """Create a new container with container 2 as base and container 1 written over it.""" + flat_container = InstanceContainer(instance_container2.getName()) # The metadata includes id, name and definition @@ -106,15 +115,15 @@ class GCodeWriter(MeshWriter): return flat_container - ## Serialises a container stack to prepare it for writing at the end of the - # g-code. - # - # The settings are serialised, and special characters (including newline) - # are escaped. - # - # \param settings A container stack to serialise. - # \return A serialised string of the settings. def _serialiseSettings(self, stack): + """Serialises a container stack to prepare it for writing at the end of the g-code. + + The settings are serialised, and special characters (including newline) + are escaped. + + :param stack: A container stack to serialise. + :return: A serialised string of the settings. + """ container_registry = self._application.getContainerRegistry() prefix = self._setting_keyword + str(GCodeWriter.version) + " " # The prefix to put before each line. @@ -151,7 +160,7 @@ class GCodeWriter(MeshWriter): data = {"global_quality": serialized} all_setting_keys = flat_global_container.getAllKeys() - for extruder in sorted(stack.extruders.values(), key = lambda k: int(k.getMetaDataEntry("position"))): + for extruder in stack.extruderList: extruder_quality = extruder.qualityChanges if extruder_quality.getId() == "empty_quality_changes": # Same story, if quality changes is empty, create a new one diff --git a/plugins/LegacyProfileReader/LegacyProfileReader.py b/plugins/LegacyProfileReader/LegacyProfileReader.py index 87b26eb4ec..0bc82ad287 100644 --- a/plugins/LegacyProfileReader/LegacyProfileReader.py +++ b/plugins/LegacyProfileReader/LegacyProfileReader.py @@ -16,58 +16,67 @@ from UM.Settings.InstanceContainer import InstanceContainer # The new profile t from cura.ReaderWriters.ProfileReader import ProfileReader # The plug-in type to implement. -## A plugin that reads profile data from legacy Cura versions. -# -# It reads a profile from an .ini file, and performs some translations on it. -# Not all translations are correct, mind you, but it is a best effort. class LegacyProfileReader(ProfileReader): - ## Initialises the legacy profile reader. - # - # This does nothing since the only other function is basically stateless. + """A plugin that reads profile data from legacy Cura versions. + + It reads a profile from an .ini file, and performs some translations on it. + Not all translations are correct, mind you, but it is a best effort. + """ + def __init__(self): + """Initialises the legacy profile reader. + + This does nothing since the only other function is basically stateless. + """ + super().__init__() - ## Prepares the default values of all legacy settings. - # - # These are loaded from the Dictionary of Doom. - # - # \param json The JSON file to load the default setting values from. This - # should not be a URL but a pre-loaded JSON handle. - # \return A dictionary of the default values of the legacy Cura version. def prepareDefaults(self, json: Dict[str, Dict[str, str]]) -> Dict[str, str]: + """Prepares the default values of all legacy settings. + + These are loaded from the Dictionary of Doom. + + :param json: The JSON file to load the default setting values from. This + should not be a URL but a pre-loaded JSON handle. + :return: A dictionary of the default values of the legacy Cura version. + """ + defaults = {} if "defaults" in json: for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict. defaults[key] = json["defaults"][key] return defaults - ## Prepares the local variables that can be used in evaluation of computing - # new setting values from the old ones. - # - # This fills a dictionary with all settings from the legacy Cura version - # and their values, so that they can be used in evaluating the new setting - # values as Python code. - # - # \param config_parser The ConfigParser that finds the settings in the - # legacy profile. - # \param config_section The section in the profile where the settings - # should be found. - # \param defaults The default values for all settings in the legacy Cura. - # \return A set of local variables, one for each setting in the legacy - # profile. def prepareLocals(self, config_parser, config_section, defaults): + """Prepares the local variables that can be used in evaluation of computing + + new setting values from the old ones. + + This fills a dictionary with all settings from the legacy Cura version + and their values, so that they can be used in evaluating the new setting + values as Python code. + + :param config_parser: The ConfigParser that finds the settings in the + legacy profile. + :param config_section: The section in the profile where the settings + should be found. + :param defaults: The default values for all settings in the legacy Cura. + :return: A set of local variables, one for each setting in the legacy + profile. + """ copied_locals = defaults.copy() # Don't edit the original! for option in config_parser.options(config_section): copied_locals[option] = config_parser.get(config_section, option) return copied_locals - ## Reads a legacy Cura profile from a file and returns it. - # - # \param file_name The file to read the legacy Cura profile from. - # \return The legacy Cura profile that was in the file, if any. If the - # file could not be read or didn't contain a valid profile, \code None - # \endcode is returned. def read(self, file_name): + """Reads a legacy Cura profile from a file and returns it. + + :param file_name: The file to read the legacy Cura profile from. + :return: The legacy Cura profile that was in the file, if any. If the + file could not be read or didn't contain a valid profile, None is returned. + """ + if file_name.split(".")[-1] != "ini": return None global_container_stack = Application.getInstance().getGlobalContainerStack() diff --git a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py index cd0f681828..64f3e1d404 100644 --- a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py +++ b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py @@ -13,7 +13,7 @@ import UM.PluginRegistry # To mock the plug-in registry out. import UM.Settings.ContainerRegistry # To mock the container registry out. import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function. -import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module. +import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module. @pytest.fixture @@ -126,9 +126,11 @@ test_prepareLocalsNoSectionErrorData = [ ) ] -## Test cases where a key error is expected. + @pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsNoSectionErrorData) def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, defaults): + """Test cases where a key error is expected.""" + parser = configparser.ConfigParser() parser.read_dict(parser_data) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index 28535024a7..f3359a1c56 100755 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -23,9 +23,11 @@ if TYPE_CHECKING: catalog = UM.i18n.i18nCatalog("cura") -## This action allows for certain settings that are "machine only") to be modified. -# It automatically detects machine definitions that it knows how to change and attaches itself to those. class MachineSettingsAction(MachineAction): + """This action allows for certain settings that are "machine only") to be modified. + + It automatically detects machine definitions that it knows how to change and attaches itself to those. + """ def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings")) self._qml_url = "MachineSettingsAction.qml" @@ -56,9 +58,11 @@ class MachineSettingsAction(MachineAction): if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine": self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey()) - ## Triggered when the global container stack changes or when the g-code - # flavour setting is changed. def _updateHasMaterialsInContainerTree(self) -> None: + """Triggered when the global container stack changes or when the g-code + + flavour setting is changed. + """ global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return diff --git a/plugins/ModelChecker/ModelChecker.py b/plugins/ModelChecker/ModelChecker.py index 057ee14945..b482667976 100644 --- a/plugins/ModelChecker/ModelChecker.py +++ b/plugins/ModelChecker/ModelChecker.py @@ -18,8 +18,8 @@ catalog = i18nCatalog("cura") class ModelChecker(QObject, Extension): - ## Signal that gets emitted when anything changed that we need to check. onChanged = pyqtSignal() + """Signal that gets emitted when anything changed that we need to check.""" def __init__(self): super().__init__() @@ -47,11 +47,13 @@ class ModelChecker(QObject, Extension): if not isinstance(args[0], Camera): self._change_timer.start() - ## Called when plug-ins are initialized. - # - # This makes sure that we listen to changes of the material and that the - # button is created that indicates warnings with the current set-up. def _pluginsInitialized(self): + """Called when plug-ins are initialized. + + This makes sure that we listen to changes of the material and that the + button is created that indicates warnings with the current set-up. + """ + Application.getInstance().getMachineManager().rootMaterialChanged.connect(self.onChanged) self._createView() @@ -106,8 +108,12 @@ class ModelChecker(QObject, Extension): if node.callDecoration("isSliceable"): yield node - ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. def _createView(self): + """Creates the view used by show popup. + + The view is saved because of the fairly aggressive garbage collection. + """ + Logger.log("d", "Creating model checker view.") # Create the plugin dialog component diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py index 3d2a1c3f37..4e4c442b4c 100644 --- a/plugins/MonitorStage/MonitorStage.py +++ b/plugins/MonitorStage/MonitorStage.py @@ -1,72 +1,72 @@ -# Copyright (c) 2017 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -import os.path -from UM.Application import Application -from cura.Stages.CuraStage import CuraStage - - -## Stage for monitoring a 3D printing while it's printing. -class MonitorStage(CuraStage): - - def __init__(self, parent = None): - super().__init__(parent) - - # Wait until QML engine is created, otherwise creating the new QML components will fail - Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) - self._printer_output_device = None - - self._active_print_job = None - self._active_printer = None - - def _setActivePrintJob(self, print_job): - if self._active_print_job != print_job: - self._active_print_job = print_job - - def _setActivePrinter(self, printer): - if self._active_printer != printer: - if self._active_printer: - self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged) - self._active_printer = printer - if self._active_printer: - self._setActivePrintJob(self._active_printer.activePrintJob) - # Jobs might change, so we need to listen to it's changes. - self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged) - else: - self._setActivePrintJob(None) - - def _onActivePrintJobChanged(self): - self._setActivePrintJob(self._active_printer.activePrintJob) - - def _onActivePrinterChanged(self): - self._setActivePrinter(self._printer_output_device.activePrinter) - - def _onOutputDevicesChanged(self): - try: - # We assume that you are monitoring the device with the highest priority. - new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0] - if new_output_device != self._printer_output_device: - if self._printer_output_device: - try: - self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged) - except TypeError: - # Ignore stupid "Not connected" errors. - pass - - self._printer_output_device = new_output_device - - self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged) - self._setActivePrinter(self._printer_output_device.activePrinter) - except IndexError: - pass - - def _onEngineCreated(self): - # We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early) - Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) - self._onOutputDevicesChanged() - - plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId()) - if plugin_path is not None: - menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml") - main_component_path = os.path.join(plugin_path, "MonitorMain.qml") - self.addDisplayComponent("menu", menu_component_path) - self.addDisplayComponent("main", main_component_path) +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +import os.path +from UM.Application import Application +from cura.Stages.CuraStage import CuraStage + + +class MonitorStage(CuraStage): + """Stage for monitoring a 3D printing while it's printing.""" + + def __init__(self, parent = None): + super().__init__(parent) + + # Wait until QML engine is created, otherwise creating the new QML components will fail + Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) + self._printer_output_device = None + + self._active_print_job = None + self._active_printer = None + + def _setActivePrintJob(self, print_job): + if self._active_print_job != print_job: + self._active_print_job = print_job + + def _setActivePrinter(self, printer): + if self._active_printer != printer: + if self._active_printer: + self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged) + self._active_printer = printer + if self._active_printer: + self._setActivePrintJob(self._active_printer.activePrintJob) + # Jobs might change, so we need to listen to it's changes. + self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged) + else: + self._setActivePrintJob(None) + + def _onActivePrintJobChanged(self): + self._setActivePrintJob(self._active_printer.activePrintJob) + + def _onActivePrinterChanged(self): + self._setActivePrinter(self._printer_output_device.activePrinter) + + def _onOutputDevicesChanged(self): + try: + # We assume that you are monitoring the device with the highest priority. + new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0] + if new_output_device != self._printer_output_device: + if self._printer_output_device: + try: + self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged) + except TypeError: + # Ignore stupid "Not connected" errors. + pass + + self._printer_output_device = new_output_device + + self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged) + self._setActivePrinter(self._printer_output_device.activePrinter) + except IndexError: + pass + + def _onEngineCreated(self): + # We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early) + Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) + self._onOutputDevicesChanged() + + plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId()) + if plugin_path is not None: + menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml") + main_component_path = os.path.join(plugin_path, "MonitorMain.qml") + self.addDisplayComponent("menu", menu_component_path) + self.addDisplayComponent("main", main_component_path) diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py index 78da0512f7..445f7ff676 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py @@ -15,9 +15,11 @@ from cura.Settings.ExtruderManager import ExtruderManager #To get global-inherit from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -## The per object setting visibility handler ensures that only setting -# definitions that have a matching instance Container are returned as visible. class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler.SettingVisibilityHandler): + """The per object setting visibility handler ensures that only setting + + definitions that have a matching instance Container are returned as visible. + """ def __init__(self, parent = None, *args, **kwargs): super().__init__(parent = parent, *args, **kwargs) diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml index 6eaadf3e83..e834372ae9 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml @@ -32,10 +32,7 @@ Item var type = currentMeshType // set checked state of mesh type buttons - normalButton.checked = type === normalMeshType - supportMeshButton.checked = type === supportMeshType - overhangMeshButton.checked = type === infillMeshType || type === cuttingMeshType - antiOverhangMeshButton.checked = type === antiOverhangMeshType + updateMeshTypeCheckedState(type) // update active type label for (var button in meshTypeButtons.children) @@ -49,9 +46,19 @@ Item visibility_handler.addSkipResetSetting(currentMeshType) } + function updateMeshTypeCheckedState(type) + { + // set checked state of mesh type buttons + normalButton.checked = type === normalMeshType + supportMeshButton.checked = type === supportMeshType + overlapMeshButton.checked = type === infillMeshType || type === cuttingMeshType + antiOverhangMeshButton.checked = type === antiOverhangMeshType + } + function setMeshType(type) { UM.ActiveTool.setProperty("MeshType", type) + updateMeshTypeCheckedState(type) } UM.I18nCatalog { id: catalog; name: "cura"} @@ -95,7 +102,7 @@ Item Button { - id: overhangMeshButton + id: overlapMeshButton text: catalog.i18nc("@label", "Modify settings for overlaps") iconSource: UM.Theme.getIcon("pos_modify_overlaps"); property bool needBorder: true diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py index 4f0d90a8e3..77f1c33a5f 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py @@ -12,9 +12,11 @@ from UM.Settings.SettingInstance import SettingInstance from UM.Event import Event -## This tool allows the user to add & change settings per node in the scene. -# The settings per object are kept in a ContainerStack, which is linked to a node by decorator. class PerObjectSettingsTool(Tool): + """This tool allows the user to add & change settings per node in the scene. + + The settings per object are kept in a ContainerStack, which is linked to a node by decorator. + """ def __init__(self): super().__init__() self._model = None @@ -48,26 +50,31 @@ class PerObjectSettingsTool(Tool): except AttributeError: return "" - ## Gets the active extruder of the currently selected object. - # - # \return The active extruder of the currently selected object. def getSelectedActiveExtruder(self): + """Gets the active extruder of the currently selected object. + + :return: The active extruder of the currently selected object. + """ + selected_object = Selection.getSelectedObject(0) return selected_object.callDecoration("getActiveExtruder") - ## Changes the active extruder of the currently selected object. - # - # \param extruder_stack_id The ID of the extruder to print the currently - # selected object with. def setSelectedActiveExtruder(self, extruder_stack_id): + """Changes the active extruder of the currently selected object. + + :param extruder_stack_id: The ID of the extruder to print the currently + selected object with. + """ + selected_object = Selection.getSelectedObject(0) stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway. if not stack: selected_object.addDecorator(SettingOverrideDecorator()) selected_object.callDecoration("setActiveExtruder", extruder_stack_id) - ## Returns True when the mesh_type was changed, False when current mesh_type == mesh_type def setMeshType(self, mesh_type: str) -> bool: + """Returns True when the mesh_type was changed, False when current mesh_type == mesh_type""" + old_mesh_type = self.getMeshType() if old_mesh_type == mesh_type: return False diff --git a/plugins/PerObjectSettingsTool/SettingPickDialog.qml b/plugins/PerObjectSettingsTool/SettingPickDialog.qml index 4e9295c05a..28ddb7e642 100644 --- a/plugins/PerObjectSettingsTool/SettingPickDialog.qml +++ b/plugins/PerObjectSettingsTool/SettingPickDialog.qml @@ -60,16 +60,12 @@ UM.Dialog CheckBox { id: toggleShowAll - anchors { top: parent.top right: parent.right } - text: catalog.i18nc("@label:checkbox", "Show all") - checked: listview.model.showAll - onClicked: listview.model.showAll = checked } ScrollView @@ -85,7 +81,7 @@ UM.Dialog } ListView { - id:listview + id: listview model: UM.SettingDefinitionsModel { id: definitionsModel @@ -98,6 +94,7 @@ UM.Dialog excluded_settings = excluded_settings.concat(settingPickDialog.additional_excluded_settings) return excluded_settings } + showAll: toggleShowAll.checked || filterInput.text !== "" } delegate:Loader { diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index 9bf8062ffd..90f3d26cd6 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -27,9 +27,8 @@ if TYPE_CHECKING: from .Script import Script -## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated -# g-code files. class PostProcessingPlugin(QObject, Extension): + """Extension type plugin that enables pre-written scripts to post process g-code files.""" def __init__(self, parent = None) -> None: QObject.__init__(self, parent) Extension.__init__(self) @@ -69,8 +68,9 @@ class PostProcessingPlugin(QObject, Extension): except IndexError: return "" - ## Execute all post-processing scripts on the gcode. def execute(self, output_device) -> None: + """Execute all post-processing scripts on the gcode.""" + scene = Application.getInstance().getController().getScene() # If the scene does not have a gcode, do nothing if not hasattr(scene, "gcode_dict"): @@ -119,9 +119,10 @@ class PostProcessingPlugin(QObject, Extension): self.selectedIndexChanged.emit() #Ensure that settings are updated self._propertyChanged() - ## Remove a script from the active script list by index. @pyqtSlot(int) def removeScriptByIndex(self, index: int) -> None: + """Remove a script from the active script list by index.""" + self._script_list.pop(index) if len(self._script_list) - 1 < self._selected_script_index: self._selected_script_index = len(self._script_list) - 1 @@ -129,10 +130,12 @@ class PostProcessingPlugin(QObject, Extension): self.selectedIndexChanged.emit() # Ensure that settings are updated self._propertyChanged() - ## Load all scripts from all paths where scripts can be found. - # - # This should probably only be done on init. def loadAllScripts(self) -> None: + """Load all scripts from all paths where scripts can be found. + + This should probably only be done on init. + """ + if self._loaded_scripts: # Already loaded. return @@ -152,10 +155,12 @@ class PostProcessingPlugin(QObject, Extension): self.loadScripts(path) - ## Load all scripts from provided path. - # This should probably only be done on init. - # \param path Path to check for scripts. def loadScripts(self, path: str) -> None: + """Load all scripts from provided path. + + This should probably only be done on init. + :param path: Path to check for scripts. + """ if ApplicationMetadata.IsEnterpriseVersion: # Delete all __pycache__ not in installation folder, as it may present a security risk. @@ -173,8 +178,8 @@ class PostProcessingPlugin(QObject, Extension): if not is_in_installation_path: TrustBasics.removeCached(path) - ## Load all scripts in the scripts folders scripts = pkgutil.iter_modules(path = [path]) + """Load all scripts in the scripts folders""" for loader, script_name, ispkg in scripts: # Iterate over all scripts. if script_name not in sys.modules: @@ -278,9 +283,8 @@ class PostProcessingPlugin(QObject, Extension): self.scriptListChanged.emit() self._propertyChanged() - ## When the global container stack is changed, swap out the list of active - # scripts. def _onGlobalContainerStackChanged(self) -> None: + """When the global container stack is changed, swap out the list of active scripts.""" if self._global_container_stack: self._global_container_stack.metaDataChanged.disconnect(self._restoreScriptInforFromMetadata) @@ -323,8 +327,12 @@ class PostProcessingPlugin(QObject, Extension): # 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. def _createView(self) -> None: + """Creates the view used by show popup. + + The view is saved because of the fairly aggressive garbage collection. + """ + Logger.log("d", "Creating post processing plugin view.") self.loadAllScripts() @@ -340,8 +348,9 @@ class PostProcessingPlugin(QObject, Extension): # Create the save button component CuraApplication.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton")) - ## Show the (GUI) popup of the post processing plugin. def showPopup(self) -> None: + """Show the (GUI) popup of the post processing plugin.""" + if self._view is None: self._createView() if self._view is None: @@ -349,11 +358,13 @@ class PostProcessingPlugin(QObject, Extension): return self._view.show() - ## Property changed: trigger re-slice - # To do this we use the global container stack propertyChanged. - # Re-slicing is necessary for setting changes in this plugin, because the changes - # are applied only once per "fresh" gcode def _propertyChanged(self) -> None: + """Property changed: trigger re-slice + + To do this we use the global container stack propertyChanged. + Re-slicing is necessary for setting changes in this plugin, because the changes + are applied only once per "fresh" gcode + """ global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack is not None: global_container_stack.propertyChanged.emit("post_processing_plugin", "value") diff --git a/plugins/PostProcessingPlugin/Script.py b/plugins/PostProcessingPlugin/Script.py index e502f107f9..3228870dca 100644 --- a/plugins/PostProcessingPlugin/Script.py +++ b/plugins/PostProcessingPlugin/Script.py @@ -23,9 +23,10 @@ if TYPE_CHECKING: from UM.Settings.Interfaces import DefinitionContainerInterface -## Base class for scripts. All scripts should inherit the script class. @signalemitter class Script: + """Base class for scripts. All scripts should inherit the script class.""" + def __init__(self) -> None: super().__init__() self._stack = None # type: Optional[ContainerStack] @@ -78,13 +79,15 @@ class Script: if global_container_stack is not None: global_container_stack.propertyChanged.emit(key, property_name) - ## Needs to return a dict that can be used to construct a settingcategory file. - # See the example script for an example. - # It follows the same style / guides as the Uranium settings. - # Scripts can either override getSettingData directly, or use getSettingDataString - # to return a string that will be parsed as json. The latter has the benefit over - # returning a dict in that the order of settings is maintained. def getSettingData(self) -> Dict[str, Any]: + """Needs to return a dict that can be used to construct a settingcategory file. + + See the example script for an example. + It follows the same style / guides as the Uranium settings. + Scripts can either override getSettingData directly, or use getSettingDataString + to return a string that will be parsed as json. The latter has the benefit over + returning a dict in that the order of settings is maintained. + """ setting_data_as_string = self.getSettingDataString() setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict) return setting_data @@ -104,15 +107,18 @@ class Script: return self._stack.getId() return None - ## Convenience function that retrieves value of a setting from the stack. def getSettingValueByKey(self, key: str) -> Any: + """Convenience function that retrieves value of a setting from the stack.""" + if self._stack is not None: return self._stack.getProperty(key, "value") return None - ## Convenience function that finds the value in a line of g-code. - # When requesting key = x from line "G1 X100" the value 100 is returned. def getValue(self, line: str, key: str, default = None) -> Any: + """Convenience function that finds the value in a line of g-code. + + When requesting key = x from line "G1 X100" the value 100 is returned. + """ if not key in line or (';' in line and line.find(key) > line.find(';')): return default sub_part = line[line.find(key) + 1:] @@ -127,20 +133,23 @@ class Script: except ValueError: #Not a number at all. return default - ## Convenience function to produce a line of g-code. - # - # You can put in an original g-code line and it'll re-use all the values - # in that line. - # All other keyword parameters are put in the result in g-code's format. - # For instance, if you put ``G=1`` in the parameters, it will output - # ``G1``. If you put ``G=1, X=100`` in the parameters, it will output - # ``G1 X100``. The parameters G and M will always be put first. The - # parameters T and S will be put second (or first if there is no G or M). - # The rest of the parameters will be put in arbitrary order. - # \param line The original g-code line that must be modified. If not - # provided, an entirely new g-code line will be produced. - # \return A line of g-code with the desired parameters filled in. def putValue(self, line: str = "", **kwargs) -> str: + """Convenience function to produce a line of g-code. + + You can put in an original g-code line and it'll re-use all the values + in that line. + All other keyword parameters are put in the result in g-code's format. + For instance, if you put ``G=1`` in the parameters, it will output + ``G1``. If you put ``G=1, X=100`` in the parameters, it will output + ``G1 X100``. The parameters G and M will always be put first. The + parameters T and S will be put second (or first if there is no G or M). + The rest of the parameters will be put in arbitrary order. + + :param line: The original g-code line that must be modified. If not + provided, an entirely new g-code line will be produced. + :return: A line of g-code with the desired parameters filled in. + """ + #Strip the comment. comment = "" if ";" in line: @@ -179,7 +188,9 @@ class Script: return result - ## This is called when the script is executed. - # It gets a list of g-code strings and needs to return a (modified) list. def execute(self, data: List[str]) -> List[str]: + """This is called when the script is executed. + + It gets a list of g-code strings and needs to return a (modified) list. + """ raise NotImplementedError() diff --git a/plugins/PostProcessingPlugin/__init__.py b/plugins/PostProcessingPlugin/__init__.py index 8064d1132a..6ddecfac69 100644 --- a/plugins/PostProcessingPlugin/__init__.py +++ b/plugins/PostProcessingPlugin/__init__.py @@ -1,6 +1,13 @@ -# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V. +# Copyright (c) 2020 Jaime van Kessel, Ultimaker B.V. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. +# Workaround for a race condition on certain systems where there +# is a race condition between Arcus and PyQt. Importing Arcus +# first seems to prevent Sip from going into a state where it +# tries to create PyQt objects on a non-main thread. +import Arcus # @UnusedImport +import Savitar # @UnusedImport + from . import PostProcessingPlugin diff --git a/plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py b/plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py deleted file mode 100644 index 0b542f2ce7..0000000000 --- a/plugins/PostProcessingPlugin/scripts/BQ_PauseAtHeight.py +++ /dev/null @@ -1,45 +0,0 @@ -from ..Script import Script -class BQ_PauseAtHeight(Script): - def __init__(self): - super().__init__() - - def getSettingDataString(self): - return """{ - "name":"Pause at height (BQ Printers)", - "key": "BQ_PauseAtHeight", - "metadata":{}, - "version": 2, - "settings": - { - "pause_height": - { - "label": "Pause height", - "description": "At what height should the pause occur", - "unit": "mm", - "type": "float", - "default_value": 5.0 - } - } - }""" - - def execute(self, data): - pause_z = self.getSettingValueByKey("pause_height") - for layer in data: - lines = layer.split("\n") - for line in lines: - if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0: - current_z = self.getValue(line, 'Z') - if current_z is not None: - if current_z >= pause_z: - prepend_gcode = ";TYPE:CUSTOM\n" - prepend_gcode += "; -- Pause at height (%.2f mm) --\n" % pause_z - - # Insert Pause gcode - prepend_gcode += "M25 ; Pauses the print and waits for the user to resume it\n" - - index = data.index(layer) - layer = prepend_gcode + layer - data[index] = layer # Override the data of this layer with the modified data - return data - break - return data diff --git a/plugins/PostProcessingPlugin/scripts/ColorMix.py b/plugins/PostProcessingPlugin/scripts/ColorMix.py index 45b2a0ad70..dacb40e905 100644 --- a/plugins/PostProcessingPlugin/scripts/ColorMix.py +++ b/plugins/PostProcessingPlugin/scripts/ColorMix.py @@ -20,7 +20,7 @@ # Uses - # M163 - Set Mix Factor # M164 - Save Mix - saves to T2 as a unique mix - + import re #To perform the search and replace. from ..Script import Script @@ -127,7 +127,7 @@ class ColorMix(Script): firstMix = self.getSettingValueByKey("mix_start") secondMix = self.getSettingValueByKey("mix_finish") modelOfInterest = self.getSettingValueByKey("object_number") - + #get layer height layerHeight = 0 for active_layer in data: @@ -138,11 +138,11 @@ class ColorMix(Script): break if layerHeight != 0: break - + #default layerHeight if not found if layerHeight == 0: layerHeight = .2 - + #get layers to use startLayer = 0 endLayer = 0 diff --git a/plugins/PostProcessingPlugin/scripts/DisplayFilenameAndLayerOnLCD.py b/plugins/PostProcessingPlugin/scripts/DisplayFilenameAndLayerOnLCD.py index cbd131f17e..d589e63fb3 100644 --- a/plugins/PostProcessingPlugin/scripts/DisplayFilenameAndLayerOnLCD.py +++ b/plugins/PostProcessingPlugin/scripts/DisplayFilenameAndLayerOnLCD.py @@ -56,7 +56,7 @@ class DisplayFilenameAndLayerOnLCD(Script): } } }""" - + def execute(self, data): max_layer = 0 if self.getSettingValueByKey("name") != "": @@ -96,5 +96,5 @@ class DisplayFilenameAndLayerOnLCD(Script): i += 1 final_lines = "\n".join(lines) data[layer_index] = final_lines - + return data diff --git a/plugins/PostProcessingPlugin/scripts/FilamentChange.py b/plugins/PostProcessingPlugin/scripts/FilamentChange.py index 943ca30f2e..74b9687f8c 100644 --- a/plugins/PostProcessingPlugin/scripts/FilamentChange.py +++ b/plugins/PostProcessingPlugin/scripts/FilamentChange.py @@ -63,10 +63,12 @@ class FilamentChange(Script): } }""" - ## Inserts the filament change g-code at specific layer numbers. - # \param data A list of layers of g-code. - # \return A similar list, with filament change commands inserted. def execute(self, data: List[str]): + """Inserts the filament change g-code at specific layer numbers. + + :param data: A list of layers of g-code. + :return: A similar list, with filament change commands inserted. + """ layer_nums = self.getSettingValueByKey("layer_number") initial_retract = self.getSettingValueByKey("initial_retract") later_retract = self.getSettingValueByKey("later_retract") diff --git a/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py index 03cc2c31a7..aa879ef889 100644 --- a/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py +++ b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py @@ -25,7 +25,7 @@ class PauseAtHeight(Script): "label": "Pause at", "description": "Whether to pause at a certain height or at a certain layer.", "type": "enum", - "options": {"height": "Height", "layer_no": "Layer No."}, + "options": {"height": "Height", "layer_no": "Layer Number"}, "default_value": "height" }, "pause_height": @@ -49,6 +49,15 @@ class PauseAtHeight(Script): "minimum_value_warning": "1", "enabled": "pause_at == 'layer_no'" }, + "pause_method": + { + "label": "Method", + "description": "The method or gcode command to use for pausing.", + "type": "enum", + "options": {"marlin": "Marlin (M0)", "griffin": "Griffin (M0, firmware retract)", "bq": "BQ (M25)", "reprap": "RepRap (M226)", "repetier": "Repetier (@pause)"}, + "default_value": "marlin", + "value": "\\\"griffin\\\" if machine_gcode_flavor==\\\"Griffin\\\" else \\\"reprap\\\" if machine_gcode_flavor==\\\"RepRap (RepRap)\\\" else \\\"repetier\\\" if machine_gcode_flavor==\\\"Repetier\\\" else \\\"bq\\\" if \\\"BQ\\\" in machine_name or \\\"Flying Bear Ghost 4S\\\" in machine_name else \\\"marlin\\\"" + }, "disarm_timeout": { "label": "Disarm timeout", @@ -66,7 +75,8 @@ class PauseAtHeight(Script): "description": "What X location does the head move to when pausing.", "unit": "mm", "type": "float", - "default_value": 190 + "default_value": 190, + "enabled": "pause_method != \\\"griffin\\\"" }, "head_park_y": { @@ -74,7 +84,17 @@ class PauseAtHeight(Script): "description": "What Y location does the head move to when pausing.", "unit": "mm", "type": "float", - "default_value": 190 + "default_value": 190, + "enabled": "pause_method != \\\"griffin\\\"" + }, + "head_move_z": + { + "label": "Head move Z", + "description": "The Height of Z-axis retraction before parking.", + "unit": "mm", + "type": "float", + "default_value": 15.0, + "enabled": "pause_method == \\\"repetier\\\"" }, "retraction_amount": { @@ -82,7 +102,8 @@ class PauseAtHeight(Script): "description": "How much filament must be retracted at pause.", "unit": "mm", "type": "float", - "default_value": 0 + "default_value": 0, + "enabled": "pause_method != \\\"griffin\\\"" }, "retraction_speed": { @@ -90,7 +111,8 @@ class PauseAtHeight(Script): "description": "How fast to retract the filament.", "unit": "mm/s", "type": "float", - "default_value": 25 + "default_value": 25, + "enabled": "pause_method not in [\\\"griffin\\\", \\\"repetier\\\"]" }, "extrude_amount": { @@ -98,7 +120,8 @@ class PauseAtHeight(Script): "description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.", "unit": "mm", "type": "float", - "default_value": 0 + "default_value": 0, + "enabled": "pause_method != \\\"griffin\\\"" }, "extrude_speed": { @@ -106,7 +129,8 @@ class PauseAtHeight(Script): "description": "How fast to extrude the material after pause.", "unit": "mm/s", "type": "float", - "default_value": 3.3333 + "default_value": 3.3333, + "enabled": "pause_method not in [\\\"griffin\\\", \\\"repetier\\\"]" }, "redo_layer": { @@ -121,33 +145,78 @@ class PauseAtHeight(Script): "description": "Change the temperature during the pause.", "unit": "°C", "type": "int", - "default_value": 0 + "default_value": 0, + "enabled": "pause_method not in [\\\"griffin\\\", \\\"repetier\\\"]" }, "display_text": { "label": "Display Text", "description": "Text that should appear on the display while paused. If left empty, there will not be any message.", "type": "str", - "default_value": "" + "default_value": "", + "enabled": "pause_method != \\\"repetier\\\"" + }, + "machine_name": + { + "label": "Machine Type", + "description": "The name of your 3D printer model. This setting is controlled by the script and will not be visible.", + "default_value": "Unknown", + "type": "str", + "enabled": false + }, + "machine_gcode_flavor": + { + "label": "G-code flavor", + "description": "The type of g-code to be generated. This setting is controlled by the script and will not be visible.", + "type": "enum", + "options": + { + "RepRap (Marlin/Sprinter)": "Marlin", + "RepRap (Volumetric)": "Marlin (Volumetric)", + "RepRap (RepRap)": "RepRap", + "UltiGCode": "Ultimaker 2", + "Griffin": "Griffin", + "Makerbot": "Makerbot", + "BFB": "Bits from Bytes", + "MACH3": "Mach3", + "Repetier": "Repetier" + }, + "default_value": "RepRap (Marlin/Sprinter)", + "enabled": false } } }""" + ## Copy machine name and gcode flavor from global stack so we can use their value in the script stack + def initialize(self) -> None: + super().initialize() + + global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack is None or self._instance is None: + return + + for key in ["machine_name", "machine_gcode_flavor"]: + self._instance.setProperty(key, "value", global_container_stack.getProperty(key, "value")) + ## Get the X and Y values for a layer (will be used to get X and Y of the # layer after the pause). def getNextXY(self, layer: str) -> Tuple[float, float]: + """Get the X and Y values for a layer (will be used to get X and Y of the layer after the pause).""" lines = layer.split("\n") for line in lines: - if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None: - x = self.getValue(line, "X") - y = self.getValue(line, "Y") - return x, y + if line.startswith(("G0", "G1", "G2", "G3")): + if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None: + x = self.getValue(line, "X") + y = self.getValue(line, "Y") + return x, y return 0, 0 - ## Inserts the pause commands. - # \param data: List of layers. - # \return New list of layers. def execute(self, data: List[str]) -> List[str]: + """Inserts the pause commands. + + :param data: List of layers. + :return: New list of layers. + """ pause_at = self.getSettingValueByKey("pause_at") pause_height = self.getSettingValueByKey("pause_height") pause_layer = self.getSettingValueByKey("pause_layer") @@ -158,6 +227,7 @@ class PauseAtHeight(Script): extrude_speed = self.getSettingValueByKey("extrude_speed") park_x = self.getSettingValueByKey("head_park_x") park_y = self.getSettingValueByKey("head_park_y") + move_z = self.getSettingValueByKey("head_move_z") layers_started = False redo_layer = self.getSettingValueByKey("redo_layer") standby_temperature = self.getSettingValueByKey("standby_temperature") @@ -166,7 +236,14 @@ class PauseAtHeight(Script): initial_layer_height = Application.getInstance().getGlobalContainerStack().getProperty("layer_height_0", "value") display_text = self.getSettingValueByKey("display_text") - is_griffin = False + pause_method = self.getSettingValueByKey("pause_method") + pause_command = { + "marlin": self.putValue(M = 0), + "griffin": self.putValue(M = 0), + "bq": self.putValue(M = 25), + "reprap": self.putValue(M = 226), + "repetier": self.putValue("@pause now change filament and press continue printing") + }[pause_method] # T = ExtruderManager.getInstance().getActiveExtruderStack().getProperty("material_print_temperature", "value") @@ -187,8 +264,6 @@ class PauseAtHeight(Script): # Scroll each line of instruction for each layer in the G-code for line in lines: - if ";FLAVOR:Griffin" in line: - is_griffin = True # Fist positive layer reached if ";LAYER:0" in line: layers_started = True @@ -290,7 +365,22 @@ class PauseAtHeight(Script): else: prepend_gcode += ";current layer: {layer}\n".format(layer = current_layer) - if not is_griffin: + if pause_method == "repetier": + #Retraction + prepend_gcode += self.putValue(M = 83) + " ; switch to relative E values for any needed retraction\n" + if retraction_amount != 0: + prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = 6000) + "\n" + + #Move the head away + prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + " ; move up a millimeter to get out of the way\n" + prepend_gcode += self.putValue(G = 1, X = park_x, Y = park_y, F = 9000) + "\n" + if current_z < move_z: + prepend_gcode += self.putValue(G = 1, Z = current_z + move_z, F = 300) + "\n" + + #Disable the E steppers + prepend_gcode += self.putValue(M = 84, E = 0) + "\n" + + elif pause_method != "griffin": # Retraction prepend_gcode += self.putValue(M = 83) + " ; switch to relative E values for any needed retraction\n" if retraction_amount != 0: @@ -322,9 +412,40 @@ class PauseAtHeight(Script): prepend_gcode += self.putValue(M = 18, S = disarm_timeout) + " ; Set the disarm timeout\n" # Wait till the user continues printing - prepend_gcode += self.putValue(M = 0) + " ; Do the actual pause\n" + prepend_gcode += pause_command + " ; Do the actual pause\n" - if not is_griffin: + if pause_method == "repetier": + #Push the filament back, + if retraction_amount != 0: + prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = 6000) + "\n" + + # Optionally extrude material + if extrude_amount != 0: + prepend_gcode += self.putValue(G = 1, E = extrude_amount, F = 200) + "\n" + prepend_gcode += self.putValue("@info wait for cleaning nozzle from previous filament") + "\n" + prepend_gcode += self.putValue("@pause remove the waste filament from parking area and press continue printing") + "\n" + + # and retract again, the properly primes the nozzle when changing filament. + if retraction_amount != 0: + prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = 6000) + "\n" + + #Move the head back + prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n" + prepend_gcode += self.putValue(G = 1, X = x, Y = y, F = 9000) + "\n" + if retraction_amount != 0: + prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = 6000) + "\n" + + if current_extrusion_f != 0: + prepend_gcode += self.putValue(G = 1, F = current_extrusion_f) + " ; restore extrusion feedrate\n" + else: + Logger.log("w", "No previous feedrate found in gcode, feedrate for next layer(s) might be incorrect") + + prepend_gcode += self.putValue(M = 82) + "\n" + + # reset extrude value to pre pause value + prepend_gcode += self.putValue(G = 92, E = current_e) + "\n" + + elif pause_method != "griffin": if control_temperatures: # Set extruder resume temperature prepend_gcode += self.putValue(M = 109, S = int(target_temperature.get(current_t, 0))) + " ; resume temperature\n" diff --git a/plugins/PostProcessingPlugin/scripts/PauseAtHeightRepRapFirmwareDuet.py b/plugins/PostProcessingPlugin/scripts/PauseAtHeightRepRapFirmwareDuet.py deleted file mode 100644 index 79e5d8c62d..0000000000 --- a/plugins/PostProcessingPlugin/scripts/PauseAtHeightRepRapFirmwareDuet.py +++ /dev/null @@ -1,51 +0,0 @@ -from ..Script import Script - -class PauseAtHeightRepRapFirmwareDuet(Script): - - def getSettingDataString(self): - return """{ - "name": "Pause at height for RepRapFirmware DuetWifi / Duet Ethernet / Duet Maestro", - "key": "PauseAtHeightRepRapFirmwareDuet", - "metadata": {}, - "version": 2, - "settings": - { - "pause_height": - { - "label": "Pause height", - "description": "At what height should the pause occur", - "unit": "mm", - "type": "float", - "default_value": 5.0 - } - } - }""" - - def execute(self, data): - current_z = 0. - pause_z = self.getSettingValueByKey("pause_height") - - layers_started = False - for layer_number, layer in enumerate(data): - lines = layer.split("\n") - for line in lines: - if ";LAYER:0" in line: - layers_started = True - continue - - if not layers_started: - continue - - if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0: - current_z = self.getValue(line, 'Z') - if current_z != None: - if current_z >= pause_z: - prepend_gcode = ";TYPE:CUSTOM\n" - prepend_gcode += "; -- Pause at height (%.2f mm) --\n" % pause_z - prepend_gcode += self.putValue(M = 226) + "\n" - layer = prepend_gcode + layer - - data[layer_number] = layer # Override the data of this layer with the modified data - return data - break - return data diff --git a/plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py b/plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py deleted file mode 100644 index 0353574289..0000000000 --- a/plugins/PostProcessingPlugin/scripts/PauseAtHeightforRepetier.py +++ /dev/null @@ -1,178 +0,0 @@ -from UM.Logger import Logger -from ..Script import Script -class PauseAtHeightforRepetier(Script): - def __init__(self): - super().__init__() - - def getSettingDataString(self): - return """{ - "name":"Pause at height for repetier", - "key": "PauseAtHeightforRepetier", - "metadata": {}, - "version": 2, - "settings": - { - "pause_height": - { - "label": "Pause height", - "description": "At what height should the pause occur", - "unit": "mm", - "type": "float", - "default_value": 5.0 - }, - "head_park_x": - { - "label": "Park print head X", - "description": "What x location does the head move to when pausing.", - "unit": "mm", - "type": "float", - "default_value": 5.0 - }, - "head_park_y": - { - "label": "Park print head Y", - "description": "What y location does the head move to when pausing.", - "unit": "mm", - "type": "float", - "default_value": 5.0 - }, - "head_move_Z": - { - "label": "Head move Z", - "description": "The Hieght of Z-axis retraction before parking.", - "unit": "mm", - "type": "float", - "default_value": 15.0 - }, - "retraction_amount": - { - "label": "Retraction", - "description": "How much fillament must be retracted at pause.", - "unit": "mm", - "type": "float", - "default_value": 5.0 - }, - "extrude_amount": - { - "label": "Extrude amount", - "description": "How much filament should be extruded after pause. This is needed when doing a material change on Ultimaker2's to compensate for the retraction after the change. In that case 128+ is recommended.", - "unit": "mm", - "type": "float", - "default_value": 90.0 - }, - "redo_layers": - { - "label": "Redo layers", - "description": "Redo a number of previous layers after a pause to increases adhesion.", - "unit": "layers", - "type": "int", - "default_value": 0 - } - } - }""" - - def execute(self, data): - x = 0. - y = 0. - current_extrusion_f = 0 - current_z = 0. - pause_z = self.getSettingValueByKey("pause_height") - retraction_amount = self.getSettingValueByKey("retraction_amount") - extrude_amount = self.getSettingValueByKey("extrude_amount") - park_x = self.getSettingValueByKey("head_park_x") - park_y = self.getSettingValueByKey("head_park_y") - move_Z = self.getSettingValueByKey("head_move_Z") - layers_started = False - redo_layers = self.getSettingValueByKey("redo_layers") - for layer in data: - lines = layer.split("\n") - for line in lines: - if ";LAYER:0" in line: - layers_started = True - continue - - if not layers_started: - continue - - if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0: - current_z = self.getValue(line, 'Z') - if self.getValue(line, 'F') is not None and self.getValue(line, 'E') is not None: - current_extrusion_f = self.getValue(line, 'F', current_extrusion_f) - x = self.getValue(line, 'X', x) - y = self.getValue(line, 'Y', y) - if current_z is not None: - if current_z >= pause_z: - - index = data.index(layer) - prevLayer = data[index-1] - prevLines = prevLayer.split("\n") - current_e = 0. - for prevLine in reversed(prevLines): - current_e = self.getValue(prevLine, 'E', -1) - if current_e >= 0: - break - - prepend_gcode = ";TYPE:CUSTOM\n" - prepend_gcode += ";added code by post processing\n" - prepend_gcode += ";script: PauseAtHeightforRepetier.py\n" - prepend_gcode += ";current z: %f \n" % (current_z) - prepend_gcode += ";current X: %f \n" % (x) - prepend_gcode += ";current Y: %f \n" % (y) - - #Retraction - prepend_gcode += "M83\n" - if retraction_amount != 0: - prepend_gcode += "G1 E-%f F6000\n" % (retraction_amount) - - #Move the head away - prepend_gcode += "G1 Z%f F300\n" % (1 + current_z) - prepend_gcode += "G1 X%f Y%f F9000\n" % (park_x, park_y) - if current_z < move_Z: - prepend_gcode += "G1 Z%f F300\n" % (current_z + move_Z) - - #Disable the E steppers - prepend_gcode += "M84 E0\n" - #Wait till the user continues printing - prepend_gcode += "@pause now change filament and press continue printing ;Do the actual pause\n" - - #Push the filament back, - if retraction_amount != 0: - prepend_gcode += "G1 E%f F6000\n" % (retraction_amount) - - # Optionally extrude material - if extrude_amount != 0: - prepend_gcode += "G1 E%f F200\n" % (extrude_amount) - prepend_gcode += "@info wait for cleaning nozzle from previous filament\n" - prepend_gcode += "@pause remove the waste filament from parking area and press continue printing\n" - - # and retract again, the properly primes the nozzle when changing filament. - if retraction_amount != 0: - prepend_gcode += "G1 E-%f F6000\n" % (retraction_amount) - - #Move the head back - prepend_gcode += "G1 Z%f F300\n" % (1 + current_z) - prepend_gcode +="G1 X%f Y%f F9000\n" % (x, y) - if retraction_amount != 0: - prepend_gcode +="G1 E%f F6000\n" % (retraction_amount) - - if current_extrusion_f != 0: - prepend_gcode += self.putValue(G=1, F=current_extrusion_f) + " ; restore extrusion feedrate\n" - else: - Logger.log("w", "No previous feedrate found in gcode, feedrate for next layer(s) might be incorrect") - - prepend_gcode +="M82\n" - - # reset extrude value to pre pause value - prepend_gcode +="G92 E%f\n" % (current_e) - - layer = prepend_gcode + layer - - # include a number of previous layers - for i in range(1, redo_layers + 1): - prevLayer = data[index-i] - layer = prevLayer + layer - - data[index] = layer #Override the data of this layer with the modified data - return data - break - return data diff --git a/plugins/PostProcessingPlugin/scripts/RetractContinue.py b/plugins/PostProcessingPlugin/scripts/RetractContinue.py index e437439287..3e095bd395 100644 --- a/plugins/PostProcessingPlugin/scripts/RetractContinue.py +++ b/plugins/PostProcessingPlugin/scripts/RetractContinue.py @@ -5,8 +5,10 @@ import math from ..Script import Script -## Continues retracting during all travel moves. + class RetractContinue(Script): + """Continues retracting during all travel moves.""" + def getSettingDataString(self): return """{ "name": "Retract Continue", diff --git a/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py b/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py index 68d697e470..a0c3648304 100644 --- a/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py +++ b/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py @@ -5,11 +5,14 @@ import re #To perform the search and replace. from ..Script import Script -## Performs a search-and-replace on all g-code. -# -# Due to technical limitations, the search can't cross the border between -# layers. + class SearchAndReplace(Script): + """Performs a search-and-replace on all g-code. + + Due to technical limitations, the search can't cross the border between + layers. + """ + def getSettingDataString(self): return """{ "name": "Search and Replace", diff --git a/plugins/PostProcessingPlugin/scripts/Stretch.py b/plugins/PostProcessingPlugin/scripts/Stretch.py index 480ba60606..e56a9f48b1 100644 --- a/plugins/PostProcessingPlugin/scripts/Stretch.py +++ b/plugins/PostProcessingPlugin/scripts/Stretch.py @@ -289,6 +289,13 @@ class Stretcher: self.layergcode = self.layergcode + sout + "\n" ipos = ipos + 1 else: + # The command is intended to be passed through unmodified via + # the comment field. In the case of an extruder only move, though, + # the extruder and potentially the feed rate are modified. + # We need to update self.outpos accordingly so that subsequent calls + # to stepToGcode() knows about the extruder and feed rate change. + self.outpos.step_e = layer_steps[i].step_e + self.outpos.step_f = layer_steps[i].step_f self.layergcode = self.layergcode + layer_steps[i].comment + "\n" def workOnSequence(self, orig_seq, modif_seq): diff --git a/plugins/PostProcessingPlugin/scripts/UsePreviousProbeMeasurements.py b/plugins/PostProcessingPlugin/scripts/UsePreviousProbeMeasurements.py index 271cb57100..62989f6c7e 100644 --- a/plugins/PostProcessingPlugin/scripts/UsePreviousProbeMeasurements.py +++ b/plugins/PostProcessingPlugin/scripts/UsePreviousProbeMeasurements.py @@ -30,7 +30,7 @@ class UsePreviousProbeMeasurements(Script): } } }""" - + def execute(self, data): text = "M501 ;load bed level data\nM420 S1 ;enable bed leveling" if self.getSettingValueByKey("use_previous_measurements"): diff --git a/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py b/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py index 360ea344ca..70fda32692 100644 --- a/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/tests/TestPostProcessingPlugin.py @@ -1,3 +1,5 @@ +# Copyright (c) 2020 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. import os import sys diff --git a/plugins/PostProcessingPlugin/tests/__init__.py b/plugins/PostProcessingPlugin/tests/__init__.py index e69de29bb2..a29c371409 100644 --- a/plugins/PostProcessingPlugin/tests/__init__.py +++ b/plugins/PostProcessingPlugin/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2020 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. \ No newline at end of file diff --git a/plugins/PrepareStage/PrepareStage.py b/plugins/PrepareStage/PrepareStage.py index c2dee9693b..2d7ee9ee4f 100644 --- a/plugins/PrepareStage/PrepareStage.py +++ b/plugins/PrepareStage/PrepareStage.py @@ -1,19 +1,21 @@ -# Copyright (c) 2019 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import os.path -from UM.Application import Application -from UM.PluginRegistry import PluginRegistry -from cura.Stages.CuraStage import CuraStage - -## Stage for preparing model (slicing). -class PrepareStage(CuraStage): - def __init__(self, parent = None): - super().__init__(parent) - Application.getInstance().engineCreatedSignal.connect(self._engineCreated) - - def _engineCreated(self): - menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml") - main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMain.qml") - self.addDisplayComponent("menu", menu_component_path) +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import os.path +from UM.Application import Application +from UM.PluginRegistry import PluginRegistry +from cura.Stages.CuraStage import CuraStage + + +class PrepareStage(CuraStage): + """Stage for preparing model (slicing).""" + + def __init__(self, parent = None): + super().__init__(parent) + Application.getInstance().engineCreatedSignal.connect(self._engineCreated) + + def _engineCreated(self): + menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml") + main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMain.qml") + self.addDisplayComponent("menu", menu_component_path) self.addDisplayComponent("main", main_component_path) \ No newline at end of file diff --git a/plugins/PreviewStage/PreviewStage.py b/plugins/PreviewStage/PreviewStage.py index 1c487c8340..88f432ef9b 100644 --- a/plugins/PreviewStage/PreviewStage.py +++ b/plugins/PreviewStage/PreviewStage.py @@ -12,37 +12,45 @@ if TYPE_CHECKING: from UM.View.View import View -## Displays a preview of what you're about to print. -# -# The Python component of this stage just loads PreviewMain.qml for display -# when the stage is selected, and makes sure that it reverts to the previous -# view when the previous stage is activated. class PreviewStage(CuraStage): + """Displays a preview of what you're about to print. + + The Python component of this stage just loads PreviewMain.qml for display + when the stage is selected, and makes sure that it reverts to the previous + view when the previous stage is activated. + """ + def __init__(self, application: QtApplication, parent = None) -> None: super().__init__(parent) self._application = application self._application.engineCreatedSignal.connect(self._engineCreated) self._previously_active_view = None # type: Optional[View] - ## When selecting the stage, remember which was the previous view so that - # we can revert to that view when we go out of the stage later. def onStageSelected(self) -> None: + """When selecting the stage, remember which was the previous view so that + + we can revert to that view when we go out of the stage later. + """ self._previously_active_view = self._application.getController().getActiveView() - ## Called when going to a different stage (away from the Preview Stage). - # - # When going to a different stage, the view should be reverted to what it - # was before. Normally, that just reverts it to solid view. def onStageDeselected(self) -> None: + """Called when going to a different stage (away from the Preview Stage). + + When going to a different stage, the view should be reverted to what it + was before. Normally, that just reverts it to solid view. + """ + if self._previously_active_view is not None: self._application.getController().setActiveView(self._previously_active_view.getPluginId()) self._previously_active_view = None - ## Delayed load of the QML files. - # - # We need to make sure that the QML engine is running before we can load - # these. def _engineCreated(self) -> None: + """Delayed load of the QML files. + + We need to make sure that the QML engine is running before we can load + these. + """ + plugin_path = self._application.getPluginRegistry().getPluginPath(self.getPluginId()) if plugin_path is not None: menu_component_path = os.path.join(plugin_path, "PreviewMenu.qml") diff --git a/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py index cf889ebb12..7b3363308d 100644 --- a/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py +++ b/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py @@ -10,12 +10,14 @@ import glob import os import subprocess -## Support for removable devices on Linux. -# -# TODO: This code uses the most basic interfaces for handling this. -# We should instead use UDisks2 to handle mount/unmount and hotplugging events. -# + class LinuxRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): + """Support for removable devices on Linux. + + TODO: This code uses the most basic interfaces for handling this. + We should instead use UDisks2 to handle mount/unmount and hotplugging events. + """ + def checkRemovableDrives(self): drives = {} for volume in glob.glob("/media/*"): diff --git a/plugins/RemovableDriveOutputDevice/OSXRemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/OSXRemovableDrivePlugin.py index 0d2c474e42..70e2107898 100644 --- a/plugins/RemovableDriveOutputDevice/OSXRemovableDrivePlugin.py +++ b/plugins/RemovableDriveOutputDevice/OSXRemovableDrivePlugin.py @@ -9,8 +9,10 @@ import os import plistlib -## Support for removable devices on Mac OSX + class OSXRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): + """Support for removable devices on Mac OSX""" + def checkRemovableDrives(self): drives = {} p = subprocess.Popen(["system_profiler", "SPUSBDataType", "-xml"], stdout = subprocess.PIPE, stderr = subprocess.PIPE) diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py index c81e4a76bc..46f38500ee 100644 --- a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py +++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py @@ -28,17 +28,19 @@ class RemovableDriveOutputDevice(OutputDevice): self._writing = False self._stream = None - ## Request the specified nodes to be written to the removable drive. - # - # \param nodes A collection of scene nodes that should be written to the - # removable drive. - # \param file_name \type{string} A suggestion for the file name to write - # to. If none is provided, a file name will be made from the names of the - # meshes. - # \param limit_mimetypes Should we limit the available MIME types to the - # MIME types available to the currently active machine? - # def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): + """Request the specified nodes to be written to the removable drive. + + :param nodes: A collection of scene nodes that should be written to the + removable drive. + :param file_name: :type{string} A suggestion for the file name to write to. + If none is provided, a file name will be made from the names of the + meshes. + :param limit_mimetypes: Should we limit the available MIME types to the + MIME types available to the currently active machine? + + """ + filter_by_machine = True # This plugin is intended to be used by machine (regardless of what it was told to do) if self._writing: raise OutputDeviceError.DeviceBusyError() @@ -106,14 +108,14 @@ class RemovableDriveOutputDevice(OutputDevice): Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e)) raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status Don't translate the XML tags or !", "Could not save to {0}: {1}").format(file_name, str(e))) from e - ## Generate a file name automatically for the specified nodes to be saved - # in. - # - # The name generated will be the name of one of the nodes. Which node that - # is can not be guaranteed. - # - # \param nodes A collection of nodes for which to generate a file name. def _automaticFileName(self, nodes): + """Generate a file name automatically for the specified nodes to be saved in. + + The name generated will be the name of one of the nodes. Which node that + is can not be guaranteed. + + :param nodes: A collection of nodes for which to generate a file name. + """ for root in nodes: for child in BreadthFirstIterator(root): if child.getMeshData(): diff --git a/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py index 8a183c25f4..ddcabd7311 100644 --- a/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py +++ b/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py @@ -42,8 +42,9 @@ ctypes.windll.kernel32.DeviceIoControl.argtypes = [ #type: ignore ctypes.windll.kernel32.DeviceIoControl.restype = wintypes.BOOL #type: ignore -## Removable drive support for windows class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): + """Removable drive support for windows""" + def checkRemovableDrives(self): drives = {} diff --git a/plugins/SentryLogger/SentryLogger.py b/plugins/SentryLogger/SentryLogger.py index 51e77ad589..29230abb1f 100644 --- a/plugins/SentryLogger/SentryLogger.py +++ b/plugins/SentryLogger/SentryLogger.py @@ -20,7 +20,7 @@ class SentryLogger(LogOutput): # processed and ready for sending. # Note that this only prepares them for sending. It only sends them when the user actually agrees to sending the # information. - + _levels = { "w": "warning", "i": "info", @@ -32,11 +32,13 @@ class SentryLogger(LogOutput): def __init__(self) -> None: super().__init__() self._show_once = set() # type: Set[str] - - ## Log the message to the sentry hub as a breadcrumb - # \param log_type "e" (error), "i"(info), "d"(debug), "w"(warning) or "c"(critical) (can postfix with "_once") - # \param message String containing message to be logged + def log(self, log_type: str, message: str) -> None: + """Log the message to the sentry hub as a breadcrumb + + :param log_type: "e" (error), "i"(info), "d"(debug), "w"(warning) or "c"(critical) (can postfix with "_once") + :param message: String containing message to be logged + """ level = self._translateLogType(log_type) message = CrashHandler.pruneSensitiveData(message) if level is None: diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 3d824197ac..349426d463 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -49,8 +49,9 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## The preview layer view. It is used to display g-code paths. class SimulationView(CuraView): + """The preview layer view. It is used to display g-code paths.""" + # Must match SimulationViewMenuComponent.qml LAYER_VIEW_TYPE_MATERIAL_TYPE = 0 LAYER_VIEW_TYPE_LINE_TYPE = 1 @@ -295,23 +296,28 @@ class SimulationView(CuraView): self.currentPathNumChanged.emit() - ## Set the layer view type - # - # \param layer_view_type integer as in SimulationView.qml and this class def setSimulationViewType(self, layer_view_type: int) -> None: + """Set the layer view type + + :param layer_view_type: integer as in SimulationView.qml and this class + """ + if layer_view_type != self._layer_view_type: self._layer_view_type = layer_view_type self.currentLayerNumChanged.emit() - ## Return the layer view type, integer as in SimulationView.qml and this class def getSimulationViewType(self) -> int: + """Return the layer view type, integer as in SimulationView.qml and this class""" + return self._layer_view_type - ## Set the extruder opacity - # - # \param extruder_nr 0..15 - # \param opacity 0.0 .. 1.0 def setExtruderOpacity(self, extruder_nr: int, opacity: float) -> None: + """Set the extruder opacity + + :param extruder_nr: 0..15 + :param opacity: 0.0 .. 1.0 + """ + if 0 <= extruder_nr <= 15: self._extruder_opacity[extruder_nr // 4][extruder_nr % 4] = opacity self.currentLayerNumChanged.emit() @@ -375,8 +381,8 @@ class SimulationView(CuraView): scene = self.getController().getScene() self._old_max_layers = self._max_layers - ## Recalculate num max layers new_max_layers = -1 + """Recalculate num max layers""" for node in DepthFirstIterator(scene.getRoot()): # type: ignore layer_data = node.callDecoration("getLayerData") if not layer_data: @@ -452,9 +458,11 @@ class SimulationView(CuraView): busyChanged = Signal() activityChanged = Signal() - ## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created - # as this caused some issues. def getProxy(self, engine, script_engine): + """Hackish way to ensure the proxy is already created + + which ensures that the layerview.qml is already created as this caused some issues. + """ if self._proxy is None: self._proxy = SimulationViewProxy(self) return self._proxy diff --git a/plugins/SliceInfoPlugin/SliceInfo.py b/plugins/SliceInfoPlugin/SliceInfo.py index c21d70819a..20a563c291 100755 --- a/plugins/SliceInfoPlugin/SliceInfo.py +++ b/plugins/SliceInfoPlugin/SliceInfo.py @@ -26,10 +26,13 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## This Extension runs in the background and sends several bits of information to the Ultimaker servers. -# The data is only sent when the user in question gave permission to do so. All data is anonymous and -# no model files are being sent (Just a SHA256 hash of the model). class SliceInfo(QObject, Extension): + """This Extension runs in the background and sends several bits of information to the Ultimaker servers. + + The data is only sent when the user in question gave permission to do so. All data is anonymous and + no model files are being sent (Just a SHA256 hash of the model). + """ + info_url = "https://stats.ultimaker.com/api/cura" def __init__(self, parent = None): @@ -54,9 +57,11 @@ class SliceInfo(QObject, Extension): if self._more_info_dialog is None: self._more_info_dialog = self._createDialog("MoreInfoWindow.qml") - ## 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. def messageActionTriggered(self, message_id, action_id): + """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. + """ self._application.getPreferences().setValue("info/asked_send_slice_info", True) if action_id == "MoreInfo": self.showMoreInfoDialog() @@ -96,7 +101,7 @@ class SliceInfo(QObject, Extension): user_modified_setting_keys = set() # type: Set[str] - for stack in [global_stack] + list(global_stack.extruders.values()): + for stack in [global_stack] + global_stack.extruderList: # Get all settings in user_changes and quality_changes all_keys = stack.userChanges.getAllKeys() | stack.qualityChanges.getAllKeys() user_modified_setting_keys |= all_keys @@ -147,7 +152,7 @@ class SliceInfo(QObject, Extension): # add extruder specific data to slice info data["extruders"] = [] - extruders = list(global_stack.extruders.values()) + extruders = global_stack.extruderList extruders = sorted(extruders, key = lambda extruder: extruder.getMetaDataEntry("position")) for extruder in extruders: diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index bfe803f224..dc88265d68 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -33,9 +33,10 @@ import math catalog = i18nCatalog("cura") -## Standard view for mesh models. class SolidView(View): + """Standard view for mesh models.""" + _show_xray_warning_preference = "view/show_xray_warning" def __init__(self): diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py index 81158978b0..9a8e7f5dfe 100644 --- a/plugins/Toolbox/src/AuthorsModel.py +++ b/plugins/Toolbox/src/AuthorsModel.py @@ -9,8 +9,12 @@ from PyQt5.QtCore import Qt, pyqtProperty from UM.Qt.ListModel import ListModel -## Model that holds cura packages. By setting the filter property the instances held by this model can be changed. class AuthorsModel(ListModel): + """Model that holds cura packages. + + By setting the filter property the instances held by this model can be changed. + """ + def __init__(self, parent = None) -> None: super().__init__(parent) @@ -67,9 +71,11 @@ class AuthorsModel(ListModel): filtered_items.sort(key = lambda k: k["name"]) self.setItems(filtered_items) - ## Set the filter of this model based on a string. - # \param filter_dict \type{Dict} Dictionary to do the filtering by. def setFilter(self, filter_dict: Dict[str, str]) -> None: + """Set the filter of this model based on a string. + + :param filter_dict: Dictionary to do the filtering by. + """ if filter_dict != self._filter: self._filter = filter_dict self._update() diff --git a/plugins/Toolbox/src/CloudApiModel.py b/plugins/Toolbox/src/CloudApiModel.py index 3386cffb51..bef37d8173 100644 --- a/plugins/Toolbox/src/CloudApiModel.py +++ b/plugins/Toolbox/src/CloudApiModel.py @@ -1,13 +1,13 @@ from typing import Union from cura import ApplicationMetadata -from cura.UltimakerCloud import UltimakerCloudAuthentication +from cura.UltimakerCloud import UltimakerCloudConstants class CloudApiModel: sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int] - cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: str - cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str + cloud_api_version = UltimakerCloudConstants.CuraCloudAPIVersion # type: str + cloud_api_root = UltimakerCloudConstants.CuraCloudAPIRoot # type: str api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format( cloud_api_root = cloud_api_root, cloud_api_version = cloud_api_version, @@ -20,9 +20,9 @@ class CloudApiModel: cloud_api_version=cloud_api_version, ) - ## https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id} @classmethod def userPackageUrl(cls, package_id: str) -> str: + """https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}""" return (CloudApiModel.api_url_user_packages + "/{package_id}").format( package_id=package_id diff --git a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py index 9c372096af..7c39354317 100644 --- a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py +++ b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py @@ -62,11 +62,11 @@ class CloudPackageChecker(QObject): def _getUserSubscribedPackages(self) -> None: self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING) - Logger.debug("Requesting subscribed packages metadata from server.") url = CloudApiModel.api_url_user_packages self._application.getHttpRequestManager().get(url, callback = self._onUserPackagesRequestFinished, error_callback = self._onUserPackagesRequestFinished, + timeout=10, scope = self._scope) def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None: diff --git a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py index ddf1a39e78..cee2f6318a 100644 --- a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py +++ b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py @@ -8,9 +8,11 @@ from UM.Signal import Signal from .SubscribedPackagesModel import SubscribedPackagesModel -## Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's -# choices are emitted on the `packageMutations` Signal. class DiscrepanciesPresenter(QObject): + """Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's + + choices are emitted on the `packageMutations` Signal. + """ def __init__(self, app: QtApplication) -> None: super().__init__(app) diff --git a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py index a5d6eee0b6..635cd89af2 100644 --- a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py +++ b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py @@ -16,9 +16,11 @@ from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope from .SubscribedPackagesModel import SubscribedPackagesModel -## Downloads a set of packages from the Ultimaker Cloud Marketplace -# use download() exactly once: should not be used for multiple sets of downloads since this class contains state class DownloadPresenter: + """Downloads a set of packages from the Ultimaker Cloud Marketplace + + use download() exactly once: should not be used for multiple sets of downloads since this class contains state + """ DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB diff --git a/plugins/Toolbox/src/CloudSync/LicensePresenter.py b/plugins/Toolbox/src/CloudSync/LicensePresenter.py index 778a36fbde..9a68c93d71 100644 --- a/plugins/Toolbox/src/CloudSync/LicensePresenter.py +++ b/plugins/Toolbox/src/CloudSync/LicensePresenter.py @@ -43,10 +43,12 @@ class LicensePresenter(QObject): self._compatibility_dialog_path = "resources/qml/dialogs/ToolboxLicenseDialog.qml" - ## Show a license dialog for multiple packages where users can read a license and accept or decline them - # \param plugin_path: Root directory of the Toolbox plugin - # \param packages: Dict[package id, file path] def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None: + """Show a license dialog for multiple packages where users can read a license and accept or decline them + + :param plugin_path: Root directory of the Toolbox plugin + :param packages: Dict[package id, file path] + """ if self._presented: Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__) return diff --git a/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py b/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py index 6e2bc53e7e..d0222029fd 100644 --- a/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py +++ b/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py @@ -3,9 +3,11 @@ from UM.Message import Message from cura.CuraApplication import CuraApplication -## Presents a dialog telling the user that a restart is required to apply changes -# Since we cannot restart Cura, the app is closed instead when the button is clicked class RestartApplicationPresenter: + """Presents a dialog telling the user that a restart is required to apply changes + + Since we cannot restart Cura, the app is closed instead when the button is clicked + """ def __init__(self, app: CuraApplication) -> None: self._app = app self._i18n_catalog = i18nCatalog("cura") diff --git a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py index fc3dfaea38..5693b82ded 100644 --- a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py +++ b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py @@ -16,20 +16,23 @@ from .RestartApplicationPresenter import RestartApplicationPresenter from .SubscribedPackagesModel import SubscribedPackagesModel -## Orchestrates the synchronizing of packages from the user account to the installed packages -# Example flow: -# - CloudPackageChecker compares a list of packages the user `subscribed` to in their account -# If there are `discrepancies` between the account and locally installed packages, they are emitted -# - DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations` -# the user selected to be performed -# - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed -# - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads -# - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to -# be installed. It emits the `licenseAnswers` signal for accept or declines -# - The CloudApiClient removes the declined packages from the account -# - The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files. -# - The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect class SyncOrchestrator(Extension): + """Orchestrates the synchronizing of packages from the user account to the installed packages + + Example flow: + + - CloudPackageChecker compares a list of packages the user `subscribed` to in their account + If there are `discrepancies` between the account and locally installed packages, they are emitted + - DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations` + the user selected to be performed + - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed + - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads + - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to + be installed. It emits the `licenseAnswers` signal for accept or declines + - The CloudApiClient removes the declined packages from the account + - The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files. + - The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect + """ def __init__(self, app: CuraApplication) -> None: super().__init__() @@ -63,10 +66,12 @@ class SyncOrchestrator(Extension): self._download_presenter.done.connect(self._onDownloadFinished) self._download_presenter.download(mutations) - ## Called when a set of packages have finished downloading - # \param success_items: Dict[package_id, Dict[str, str]] - # \param error_items: List[package_id] def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None: + """Called when a set of packages have finished downloading + + :param success_items:: Dict[package_id, Dict[str, str]] + :param error_items:: List[package_id] + """ if error_items: message = i18n_catalog.i18nc("@info:generic", "{} plugins failed to download".format(len(error_items))) self._showErrorMessage(message) @@ -96,7 +101,8 @@ class SyncOrchestrator(Extension): if has_changes: self._restart_presenter.present() - ## Logs an error and shows it to the user def _showErrorMessage(self, text: str): + """Logs an error and shows it to the user""" + Logger.error(text) Message(text, lifetime=0).show() diff --git a/plugins/Toolbox/src/ConfigsModel.py b/plugins/Toolbox/src/ConfigsModel.py index a92f9c0d93..a53817653f 100644 --- a/plugins/Toolbox/src/ConfigsModel.py +++ b/plugins/Toolbox/src/ConfigsModel.py @@ -6,8 +6,9 @@ from PyQt5.QtCore import Qt from UM.Qt.ListModel import ListModel -## Model that holds supported configurations (for material/quality packages). class ConfigsModel(ListModel): + """Model that holds supported configurations (for material/quality packages).""" + def __init__(self, parent = None): super().__init__(parent) diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py index c84e0da5d0..85811a9eb4 100644 --- a/plugins/Toolbox/src/PackagesModel.py +++ b/plugins/Toolbox/src/PackagesModel.py @@ -12,8 +12,12 @@ from UM.Qt.ListModel import ListModel from .ConfigsModel import ConfigsModel -## Model that holds Cura packages. By setting the filter property the instances held by this model can be changed. class PackagesModel(ListModel): + """Model that holds Cura packages. + + By setting the filter property the instances held by this model can be changed. + """ + def __init__(self, parent = None): super().__init__(parent) @@ -131,9 +135,11 @@ class PackagesModel(ListModel): filtered_items.sort(key = lambda k: k["name"]) self.setItems(filtered_items) - ## Set the filter of this model based on a string. - # \param filter_dict \type{Dict} Dictionary to do the filtering by. def setFilter(self, filter_dict: Dict[str, str]) -> None: + """Set the filter of this model based on a string. + + :param filter_dict: Dictionary to do the filtering by. + """ if filter_dict != self._filter: self._filter = filter_dict self._update() diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 3b1f85a69e..876ca586a7 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -1,4 +1,4 @@ -# Copyright (c) 2019 Ultimaker B.V. +# Copyright (c) 2020 Ultimaker B.V. # Toolbox is released under the terms of the LGPLv3 or higher. import json @@ -37,10 +37,10 @@ try: except ImportError: CuraMarketplaceRoot = DEFAULT_MARKETPLACE_ROOT -# todo Remove license and download dialog, use SyncOrchestrator instead -## Provides a marketplace for users to download plugins an materials class Toolbox(QObject, Extension): + """Provides a marketplace for users to download plugins an materials""" + def __init__(self, application: CuraApplication) -> None: super().__init__() @@ -135,8 +135,9 @@ class Toolbox(QObject, Extension): closeLicenseDialog = pyqtSignal() uninstallVariablesChanged = pyqtSignal() - ## Go back to the start state (welcome screen or loading if no login required) def _restart(self): + """Go back to the start state (welcome screen or loading if no login required)""" + # For an Essentials build, login is mandatory if not self._application.getCuraAPI().account.isLoggedIn and ApplicationMetadata.IsEnterpriseVersion: self.setViewPage("welcome") @@ -231,7 +232,7 @@ class Toolbox(QObject, Extension): "licenseModel": self._license_model }) if not dialog: - raise Exception("Failed to create Marketplace dialog") + return None return dialog def _convertPluginMetadata(self, plugin_data: Dict[str, Any]) -> Optional[Dict[str, Any]]: @@ -311,10 +312,13 @@ class Toolbox(QObject, Extension): self.restartRequiredChanged.emit() return package_id - ## Check package usage and uninstall - # If the package is in use, you'll get a confirmation dialog to set everything to default @pyqtSlot(str) def checkPackageUsageAndUninstall(self, package_id: str) -> None: + """Check package usage and uninstall + + If the package is in use, you'll get a confirmation dialog to set everything to default + """ + package_used_materials, package_used_qualities = self._package_manager.getMachinesUsingPackage(package_id) if package_used_materials or package_used_qualities: # Set up "uninstall variables" for resetMaterialsQualitiesAndUninstall @@ -352,10 +356,13 @@ class Toolbox(QObject, Extension): if self._confirm_reset_dialog is not None: self._confirm_reset_dialog.close() - ## Uses "uninstall variables" to reset qualities and materials, then uninstall - # It's used as an action on Confirm reset on Uninstall @pyqtSlot() def resetMaterialsQualitiesAndUninstall(self) -> None: + """Uses "uninstall variables" to reset qualities and materials, then uninstall + + It's used as an action on Confirm reset on Uninstall + """ + application = CuraApplication.getInstance() machine_manager = application.getMachineManager() container_tree = ContainerTree.getInstance() @@ -418,8 +425,9 @@ class Toolbox(QObject, Extension): self._restart_required = True self.restartRequiredChanged.emit() - ## Actual update packages that are in self._to_update def _update(self) -> None: + """Actual update packages that are in self._to_update""" + if self._to_update: plugin_id = self._to_update.pop(0) remote_package = self.getRemotePackage(plugin_id) @@ -433,9 +441,10 @@ class Toolbox(QObject, Extension): if self._to_update: self._application.callLater(self._update) - ## Update a plugin by plugin_id @pyqtSlot(str) def update(self, plugin_id: str) -> None: + """Update a plugin by plugin_id""" + self._to_update.append(plugin_id) self._application.callLater(self._update) @@ -714,9 +723,10 @@ class Toolbox(QObject, Extension): self._active_package = package self.activePackageChanged.emit() - ## The active package is the package that is currently being downloaded @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged) def activePackage(self) -> Optional[QObject]: + """The active package is the package that is currently being downloaded""" + return self._active_package def setViewCategory(self, category: str = "plugin") -> None: @@ -724,7 +734,7 @@ class Toolbox(QObject, Extension): self._view_category = category self.viewChanged.emit() - ## Function explicitly defined so that it can be called through the callExtensionsMethod + # Function explicitly defined so that it can be called through the callExtensionsMethod # which cannot receive arguments. def setViewCategoryToMaterials(self) -> None: self.setViewCategory("material") diff --git a/plugins/TrimeshReader/TrimeshReader.py b/plugins/TrimeshReader/TrimeshReader.py index 6ed7435f88..cbec2e2482 100644 --- a/plugins/TrimeshReader/TrimeshReader.py +++ b/plugins/TrimeshReader/TrimeshReader.py @@ -22,8 +22,10 @@ from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator # Adde if TYPE_CHECKING: from UM.Scene.SceneNode import SceneNode -## Class that leverages Trimesh to import files. + class TrimeshReader(MeshReader): + """Class that leverages Trimesh to import files.""" + def __init__(self) -> None: super().__init__() @@ -79,11 +81,13 @@ class TrimeshReader(MeshReader): ) ) - ## Reads a file using Trimesh. - # \param file_name The file path. This is assumed to be one of the file - # types that Trimesh can read. It will not be checked again. - # \return A scene node that contains the file's contents. def _read(self, file_name: str) -> Union["SceneNode", List["SceneNode"]]: + """Reads a file using Trimesh. + + :param file_name: The file path. This is assumed to be one of the file + types that Trimesh can read. It will not be checked again. + :return: A scene node that contains the file's contents. + """ # CURA-6739 # GLTF files are essentially JSON files. If you directly give a file name to trimesh.load(), it will # try to figure out the format, but for GLTF, it loads it as a binary file with flags "rb", and the json.load() @@ -130,13 +134,14 @@ class TrimeshReader(MeshReader): node.setParent(group_node) return group_node - ## Converts a Trimesh to Uranium's MeshData. - # \param tri_node A Trimesh containing the contents of a file that was - # just read. - # \param file_name The full original filename used to watch for changes - # \return Mesh data from the Trimesh in a way that Uranium can understand - # it. def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData: + """Converts a Trimesh to Uranium's MeshData. + + :param tri_node: A Trimesh containing the contents of a file that was just read. + :param file_name: The full original filename used to watch for changes + :return: Mesh data from the Trimesh in a way that Uranium can understand it. + """ + tri_faces = tri_node.faces tri_vertices = tri_node.vertices diff --git a/plugins/UFPWriter/UFPWriter.py b/plugins/UFPWriter/UFPWriter.py index bcafc7545c..3c241670b0 100644 --- a/plugins/UFPWriter/UFPWriter.py +++ b/plugins/UFPWriter/UFPWriter.py @@ -97,7 +97,7 @@ class UFPWriter(MeshWriter): Logger.log("w", "The material extension: %s was already added", material_extension) added_materials = [] - for extruder_stack in global_stack.extruders.values(): + for extruder_stack in global_stack.extruderList: material = extruder_stack.material try: material_file_name = material.getMetaData()["base_file"] + ".xml.fdm_material" diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 1c9670d87f..713ee25170 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -6,11 +6,15 @@ from time import time from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any, cast from PyQt5.QtCore import QUrl -from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from UM.Logger import Logger +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.API import Account -from cura.UltimakerCloud import UltimakerCloudAuthentication +from cura.CuraApplication import CuraApplication +from cura.UltimakerCloud import UltimakerCloudConstants +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope from .ToolPathUploader import ToolPathUploader from ..Models.BaseModel import BaseModel from ..Models.Http.CloudClusterResponse import CloudClusterResponse @@ -20,71 +24,103 @@ from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest from ..Models.Http.CloudPrintResponse import CloudPrintResponse -## The generic type variable used to document the methods below. CloudApiClientModel = TypeVar("CloudApiClientModel", bound=BaseModel) +"""The generic type variable used to document the methods below.""" -## The cloud API client is responsible for handling the requests and responses from the cloud. -# Each method should only handle models instead of exposing Any HTTP details. class CloudApiClient: + """The cloud API client is responsible for handling the requests and responses from the cloud. + + Each method should only handle models instead of exposing Any HTTP details. + """ # The cloud URL to use for this remote cluster. - ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot + ROOT_PATH = UltimakerCloudConstants.CuraCloudAPIRoot CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH) CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH) - # In order to avoid garbage collection we keep the callbacks in this list. - _anti_gc_callbacks = [] # type: List[Callable[[], None]] + DEFAULT_REQUEST_TIMEOUT = 10 # seconds - ## Initializes a new cloud API client. - # \param account: The user's account object - # \param on_error: The callback to be called whenever we receive errors from the server. - def __init__(self, account: Account, on_error: Callable[[List[CloudError]], None]) -> None: + # In order to avoid garbage collection we keep the callbacks in this list. + _anti_gc_callbacks = [] # type: List[Callable[[Any], None]] + + def __init__(self, app: CuraApplication, on_error: Callable[[List[CloudError]], None]) -> None: + """Initializes a new cloud API client. + + :param app: + :param account: The user's account object + :param on_error: The callback to be called whenever we receive errors from the server. + """ super().__init__() - self._manager = QNetworkAccessManager() - self._account = account + self._app = app + self._account = app.getCuraAPI().account + self._scope = JsonDecoratorScope(UltimakerCloudScope(app)) + self._http = HttpRequestManager.getInstance() self._on_error = on_error self._upload = None # type: Optional[ToolPathUploader] - ## Gets the account used for the API. @property def account(self) -> Account: + """Gets the account used for the API.""" + return self._account - ## Retrieves all the clusters for the user that is currently logged in. - # \param on_finished: The function to be called after the result is parsed. def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None: + """Retrieves all the clusters for the user that is currently logged in. + + :param on_finished: The function to be called after the result is parsed. + """ + url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT) - reply = self._manager.get(self._createEmptyRequest(url)) - self._addCallback(reply, on_finished, CloudClusterResponse, failed) + self._http.get(url, + scope = self._scope, + callback = self._parseCallback(on_finished, CloudClusterResponse, failed), + error_callback = failed, + timeout = self.DEFAULT_REQUEST_TIMEOUT) - ## Retrieves the status of the given cluster. - # \param cluster_id: The ID of the cluster. - # \param on_finished: The function to be called after the result is parsed. def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None: - url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id) - reply = self._manager.get(self._createEmptyRequest(url)) - self._addCallback(reply, on_finished, CloudClusterStatus) + """Retrieves the status of the given cluster. + + :param cluster_id: The ID of the cluster. + :param on_finished: The function to be called after the result is parsed. + """ + + url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id) + self._http.get(url, + scope = self._scope, + callback = self._parseCallback(on_finished, CloudClusterStatus), + timeout = self.DEFAULT_REQUEST_TIMEOUT) - ## Requests the cloud to register the upload of a print job mesh. - # \param request: The request object. - # \param on_finished: The function to be called after the result is parsed. def requestUpload(self, request: CloudPrintJobUploadRequest, on_finished: Callable[[CloudPrintJobResponse], Any]) -> None: - url = "{}/jobs/upload".format(self.CURA_API_ROOT) - body = json.dumps({"data": request.toDict()}) - reply = self._manager.put(self._createEmptyRequest(url), body.encode()) - self._addCallback(reply, on_finished, CloudPrintJobResponse) - ## Uploads a print job tool path to the cloud. - # \param print_job: The object received after requesting an upload with `self.requestUpload`. - # \param mesh: The tool path data to be uploaded. - # \param on_finished: The function to be called after the upload is successful. - # \param on_progress: A function to be called during upload progress. It receives a percentage (0-100). - # \param on_error: A function to be called if the upload fails. + """Requests the cloud to register the upload of a print job mesh. + + :param request: The request object. + :param on_finished: The function to be called after the result is parsed. + """ + + url = "{}/jobs/upload".format(self.CURA_API_ROOT) + data = json.dumps({"data": request.toDict()}).encode() + + self._http.put(url, + scope = self._scope, + data = data, + callback = self._parseCallback(on_finished, CloudPrintJobResponse), + timeout = self.DEFAULT_REQUEST_TIMEOUT) + def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]): - self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error) + """Uploads a print job tool path to the cloud. + + :param print_job: The object received after requesting an upload with `self.requestUpload`. + :param mesh: The tool path data to be uploaded. + :param on_finished: The function to be called after the upload is successful. + :param on_progress: A function to be called during upload progress. It receives a percentage (0-100). + :param on_error: A function to be called if the upload fails. + """ + + self._upload = ToolPathUploader(self._http, print_job, mesh, on_finished, on_progress, on_error) self._upload.start() # Requests a cluster to print the given print job. @@ -93,23 +129,36 @@ class CloudApiClient: # \param on_finished: The function to be called after the result is parsed. def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None: url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id) - reply = self._manager.post(self._createEmptyRequest(url), b"") - self._addCallback(reply, on_finished, CloudPrintResponse) + self._http.post(url, + scope = self._scope, + data = b"", + callback = self._parseCallback(on_finished, CloudPrintResponse), + timeout = self.DEFAULT_REQUEST_TIMEOUT) - ## Send a print job action to the cluster for the given print job. - # \param cluster_id: The ID of the cluster. - # \param cluster_job_id: The ID of the print job within the cluster. - # \param action: The name of the action to execute. def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, data: Optional[Dict[str, Any]] = None) -> None: + + """Send a print job action to the cluster for the given print job. + + :param cluster_id: The ID of the cluster. + :param cluster_job_id: The ID of the print job within the cluster. + :param action: The name of the action to execute. + """ + body = json.dumps({"data": data}).encode() if data else b"" url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action) - self._manager.post(self._createEmptyRequest(url), body) + self._http.post(url, + scope = self._scope, + data = body, + timeout = self.DEFAULT_REQUEST_TIMEOUT) - ## We override _createEmptyRequest in order to add the user credentials. - # \param url: The URL to request - # \param content_type: The type of the body contents. def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest: + """We override _createEmptyRequest in order to add the user credentials. + + :param url: The URL to request + :param content_type: The type of the body contents. + """ + request = QNetworkRequest(QUrl(path)) if content_type: request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) @@ -118,11 +167,14 @@ class CloudApiClient: request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode()) return request - ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. - # \param reply: The reply from the server. - # \return A tuple with a status code and a dictionary. @staticmethod def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: + """Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. + + :param reply: The reply from the server. + :return: A tuple with a status code and a dictionary. + """ + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) try: response = bytes(reply.readAll()).decode() @@ -133,14 +185,15 @@ class CloudApiClient: Logger.logException("e", "Could not parse the stardust response: %s", error.toDict()) return status_code, {"errors": [error.toDict()]} - ## Parses the given models and calls the correct callback depending on the result. - # \param response: The response from the server, after being converted to a dict. - # \param on_finished: The callback in case the response is successful. - # \param model_class: The type of the model to convert the response to. It may either be a single record or a list. - def _parseModels(self, response: Dict[str, Any], - on_finished: Union[Callable[[CloudApiClientModel], Any], - Callable[[List[CloudApiClientModel]], Any]], - model_class: Type[CloudApiClientModel]) -> None: + def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any], + Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None: + """Parses the given models and calls the correct callback depending on the result. + + :param response: The response from the server, after being converted to a dict. + :param on_finished: The callback in case the response is successful. + :param model_class: The type of the model to convert the response to. It may either be a single record or a list. + """ + if "data" in response: data = response["data"] if isinstance(data, list): @@ -156,19 +209,23 @@ class CloudApiClient: else: Logger.log("e", "Cannot find data or errors in the cloud response: %s", response) - ## Creates a callback function so that it includes the parsing of the response into the correct model. - # The callback is added to the 'finished' signal of the reply. - # \param reply: The reply that should be listened to. - # \param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either - # a list or a single item. - # \param model: The type of the model to convert the response to. - def _addCallback(self, - reply: QNetworkReply, - on_finished: Union[Callable[[CloudApiClientModel], Any], - Callable[[List[CloudApiClientModel]], Any]], - model: Type[CloudApiClientModel], - on_error: Optional[Callable] = None) -> None: - def parse() -> None: + def _parseCallback(self, + on_finished: Union[Callable[[CloudApiClientModel], Any], + Callable[[List[CloudApiClientModel]], Any]], + model: Type[CloudApiClientModel], + on_error: Optional[Callable] = None) -> Callable[[QNetworkReply], None]: + + """Creates a callback function so that it includes the parsing of the response into the correct model. + + The callback is added to the 'finished' signal of the reply. + :param reply: The reply that should be listened to. + :param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either + a list or a single item. + :param model: The type of the model to convert the response to. + """ + + def parse(reply: QNetworkReply) -> None: + self._anti_gc_callbacks.remove(parse) # Don't try to parse the reply if we didn't get one @@ -184,6 +241,4 @@ class CloudApiClient: self._parseModels(response, on_finished, model) self._anti_gc_callbacks.append(parse) - reply.finished.connect(parse) - if on_error is not None: - reply.error.connect(on_error) + return parse diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 64638a0a1e..4abab245e8 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -35,11 +35,13 @@ from ..Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus I18N_CATALOG = i18nCatalog("cura") -## The cloud output device is a network output device that works remotely but has limited functionality. -# Currently it only supports viewing the printer and print job status and adding a new job to the queue. -# As such, those methods have been implemented here. -# Note that this device represents a single remote cluster, not a list of multiple clusters. class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): + """The cloud output device is a network output device that works remotely but has limited functionality. + + Currently it only supports viewing the printer and print job status and adding a new job to the queue. + As such, those methods have been implemented here. + Note that this device represents a single remote cluster, not a list of multiple clusters. + """ # The interval with which the remote cluster is checked. # We can do this relatively often as this API call is quite fast. @@ -56,11 +58,13 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): # Therefore we create a private signal used to trigger the printersChanged signal. _cloudClusterPrintersChanged = pyqtSignal() - ## Creates a new cloud output device - # \param api_client: The client that will run the API calls - # \param cluster: The device response received from the cloud API. - # \param parent: The optional parent of this output device. def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None: + """Creates a new cloud output device + + :param api_client: The client that will run the API calls + :param cluster: The device response received from the cloud API. + :param parent: The optional parent of this output device. + """ # The following properties are expected on each networked output device. # Because the cloud connection does not off all of these, we manually construct this version here. @@ -70,7 +74,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): b"name": cluster.friendly_name.encode() if cluster.friendly_name else b"", b"firmware_version": cluster.host_version.encode() if cluster.host_version else b"", b"printer_type": cluster.printer_type.encode() if cluster.printer_type else b"", - b"cluster_size": b"1" # cloud devices are always clusters of at least one + b"cluster_size": str(cluster.printer_count).encode() if cluster.printer_count else b"1" } super().__init__( @@ -99,8 +103,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._tool_path = None # type: Optional[bytes] self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse] - ## Connects this device. def connect(self) -> None: + """Connects this device.""" + if self.isConnected(): return super().connect() @@ -108,21 +113,24 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange) self._update() - ## Disconnects the device def disconnect(self) -> None: + """Disconnects the device""" + if not self.isConnected(): return super().disconnect() Logger.log("i", "Disconnected from cluster %s", self.key) CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange) - ## Resets the print job that was uploaded to force a new upload, runs whenever the user re-slices. def _onBackendStateChange(self, _: BackendState) -> None: + """Resets the print job that was uploaded to force a new upload, runs whenever the user re-slices.""" + self._tool_path = None self._uploaded_print_job = None - ## Checks whether the given network key is found in the cloud's host name def matchesNetworkKey(self, network_key: str) -> bool: + """Checks whether the given network key is found in the cloud's host name""" + # Typically, a network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local." # the host name should then be "ultimakersystem-aabbccdd0011" if network_key.startswith(str(self.clusterData.host_name or "")): @@ -133,15 +141,17 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): return True return False - ## Set all the interface elements and texts for this output device. def _setInterfaceElements(self) -> None: + """Set all the interface elements and texts for this output device.""" + self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'. self.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via Cloud")) self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud")) self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud")) - ## Called when the network data should be updated. def _update(self) -> None: + """Called when the network data should be updated.""" + super()._update() if time() - self._time_of_last_request < self.CHECK_CLUSTER_INTERVAL: return # avoid calling the cloud too often @@ -153,9 +163,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): else: self.setAuthenticationState(AuthState.NotAuthenticated) - ## Method called when HTTP request to status endpoint is finished. - # Contains both printers and print jobs statuses in a single response. def _onStatusCallFinished(self, status: CloudClusterStatus) -> None: + """Method called when HTTP request to status endpoint is finished. + + Contains both printers and print jobs statuses in a single response. + """ self._responseReceived() if status.printers != self._received_printers: self._received_printers = status.printers @@ -164,10 +176,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._received_print_jobs = status.print_jobs self._updatePrintJobs(status.print_jobs) - ## Called when Cura requests an output device to receive a (G-code) file. def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None: + """Called when Cura requests an output device to receive a (G-code) file.""" + # Show an error message if we're already sending a job. if self._progress.visible: PrintJobUploadBlockedMessage().show() @@ -187,9 +200,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): job.finished.connect(self._onPrintJobCreated) job.start() - ## Handler for when the print job was created locally. - # It can now be sent over the cloud. def _onPrintJobCreated(self, job: ExportFileJob) -> None: + """Handler for when the print job was created locally. + + It can now be sent over the cloud. + """ output = job.getOutput() self._tool_path = output # store the tool path to prevent re-uploading when printing the same file again file_name = job.getFileName() @@ -200,9 +215,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): ) self._api.requestUpload(request, self._uploadPrintJob) - ## Uploads the mesh when the print job was registered with the cloud API. - # \param job_response: The response received from the cloud API. def _uploadPrintJob(self, job_response: CloudPrintJobResponse) -> None: + """Uploads the mesh when the print job was registered with the cloud API. + + :param job_response: The response received from the cloud API. + """ if not self._tool_path: return self._onUploadError() self._progress.show() @@ -210,38 +227,45 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded, self._progress.update, self._onUploadError) - ## Requests the print to be sent to the printer when we finished uploading the mesh. def _onPrintJobUploaded(self) -> None: + """Requests the print to be sent to the printer when we finished uploading the mesh.""" + self._progress.update(100) print_job = cast(CloudPrintJobResponse, self._uploaded_print_job) self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted) - ## Shows a message when the upload has succeeded - # \param response: The response from the cloud API. def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None: + """Shows a message when the upload has succeeded + + :param response: The response from the cloud API. + """ self._progress.hide() PrintJobUploadSuccessMessage().show() self.writeFinished.emit() - ## Displays the given message if uploading the mesh has failed - # \param message: The message to display. def _onUploadError(self, message: str = None) -> None: + """Displays the given message if uploading the mesh has failed + + :param message: The message to display. + """ self._progress.hide() self._uploaded_print_job = None PrintJobUploadErrorMessage(message).show() self.writeError.emit() - ## Whether the printer that this output device represents supports print job actions via the cloud. @pyqtProperty(bool, notify=_cloudClusterPrintersChanged) def supportsPrintJobActions(self) -> bool: + """Whether the printer that this output device represents supports print job actions via the cloud.""" + if not self._printers: return False version_number = self.printers[0].firmwareVersion.split(".") firmware_version = Version([version_number[0], version_number[1], version_number[2]]) return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION - ## Set the remote print job state. def setJobState(self, print_job_uuid: str, state: str) -> None: + """Set the remote print job state.""" + self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, state) @pyqtSlot(str, name="sendJobToTop") @@ -265,18 +289,21 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): def openPrinterControlPanel(self) -> None: QDesktopServices.openUrl(QUrl(self.clusterCloudUrl)) - ## Gets the cluster response from which this device was created. @property def clusterData(self) -> CloudClusterResponse: + """Gets the cluster response from which this device was created.""" + return self._cluster - ## Updates the cluster data from the cloud. @clusterData.setter def clusterData(self, value: CloudClusterResponse) -> None: + """Updates the cluster data from the cloud.""" + self._cluster = value - ## Gets the URL on which to monitor the cluster via the cloud. @property def clusterCloudUrl(self) -> str: + """Gets the URL on which to monitor the cluster via the cloud.""" + root_url_prefix = "-staging" if self._account.is_staging else "" return "https://mycloud{}.ultimaker.com/app/jobs/{}".format(root_url_prefix, self.clusterData.cluster_id) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index f233e59fe5..322919124f 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -1,19 +1,23 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import os -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Set -from PyQt5.QtCore import QTimer +from PyQt5.QtNetwork import QNetworkReply +from PyQt5.QtWidgets import QMessageBox from UM import i18nCatalog from UM.Logger import Logger # To log errors talking to the API. from UM.Message import Message +from UM.Settings.Interfaces import ContainerInterface from UM.Signal import Signal +from UM.Util import parseBool from cura.API import Account from cura.API.Account import SyncState from cura.CuraApplication import CuraApplication from cura.Settings.CuraStackBuilder import CuraStackBuilder from cura.Settings.GlobalStack import GlobalStack +from cura.UltimakerCloud.UltimakerCloudConstants import META_UM_LINKED_TO_ACCOUNT from .CloudApiClient import CloudApiClient from .CloudOutputDevice import CloudOutputDevice from ..Models.Http.CloudClusterResponse import CloudClusterResponse @@ -21,13 +25,15 @@ from ..Models.Http.CloudClusterResponse import CloudClusterResponse class CloudOutputDeviceManager: """The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters. - + Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code. API spec is available on https://api.ultimaker.com/docs/connect/spec/. """ META_CLUSTER_ID = "um_cloud_cluster_id" + META_HOST_GUID = "host_guid" META_NETWORK_KEY = "um_network_key" + SYNC_SERVICE_NAME = "CloudOutputDeviceManager" # The translation catalog for this device. @@ -39,15 +45,22 @@ class CloudOutputDeviceManager: def __init__(self) -> None: # Persistent dict containing the remote clusters for the authenticated user. self._remote_clusters = {} # type: Dict[str, CloudOutputDevice] + + # Dictionary containing all the cloud printers loaded in Cura + self._um_cloud_printers = {} # type: Dict[str, GlobalStack] + self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account - self._api = CloudApiClient(self._account, on_error = lambda error: Logger.log("e", str(error))) + self._api = CloudApiClient(CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error))) self._account.loginStateChanged.connect(self._onLoginStateChanged) + self._removed_printers_message = None # type: Optional[Message] # Ensure we don't start twice. self._running = False self._syncing = False + CuraApplication.getInstance().getContainerRegistry().containerRemoved.connect(self._printerRemoved) + def start(self): """Starts running the cloud output device manager, thus periodically requesting cloud data.""" @@ -87,38 +100,60 @@ class CloudOutputDeviceManager: if self._syncing: return - Logger.info("Syncing cloud printer clusters") - self._syncing = True self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING) self._api.getClusters(self._onGetRemoteClustersFinished, self._onGetRemoteClusterFailed) def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None: - """Callback for when the request for getting the clusters is finished.""" + """Callback for when the request for getting the clusters is successful and finished.""" + self._um_cloud_printers = {m.getMetaDataEntry(self.META_CLUSTER_ID): m for m in + CuraApplication.getInstance().getContainerRegistry().findContainerStacks( + type = "machine") if m.getMetaDataEntry(self.META_CLUSTER_ID, None)} new_clusters = [] + all_clusters = {c.cluster_id: c for c in clusters} # type: Dict[str, CloudClusterResponse] online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse] - for device_id, cluster_data in online_clusters.items(): + # Add the new printers in Cura. + for device_id, cluster_data in all_clusters.items(): if device_id not in self._remote_clusters: new_clusters.append(cluster_data) - + if device_id in self._um_cloud_printers: + # Existing cloud printers may not have the host_guid meta-data entry. If that's the case, add it. + if not self._um_cloud_printers[device_id].getMetaDataEntry(self.META_HOST_GUID, None): + self._um_cloud_printers[device_id].setMetaDataEntry(self.META_HOST_GUID, cluster_data.host_guid) + # If a printer was previously not linked to the account and is rediscovered, mark the printer as linked + # to the current account + if not parseBool(self._um_cloud_printers[device_id].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")): + self._um_cloud_printers[device_id].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True) self._onDevicesDiscovered(new_clusters) - removed_device_keys = set(self._remote_clusters.keys()) - set(online_clusters.keys()) - for device_id in removed_device_keys: + # Hide the current removed_printers_message, if there is any + if self._removed_printers_message: + self._removed_printers_message.actionTriggered.disconnect(self._onRemovedPrintersMessageActionTriggered) + self._removed_printers_message.hide() + + # Remove the CloudOutput device for offline printers + offline_device_keys = set(self._remote_clusters.keys()) - set(online_clusters.keys()) + for device_id in offline_device_keys: self._onDiscoveredDeviceRemoved(device_id) - if new_clusters or removed_device_keys: - self.discoveredDevicesChanged.emit() + # Handle devices that were previously added in Cura but do not exist in the account anymore (i.e. they were + # removed from the account) + removed_device_keys = set(self._um_cloud_printers.keys()) - set(all_clusters.keys()) if removed_device_keys: + self._devicesRemovedFromAccount(removed_device_keys) + + if new_clusters or offline_device_keys or removed_device_keys: + self.discoveredDevicesChanged.emit() + if offline_device_keys: # If the removed device was active we should connect to the new active device self._connectToActiveMachine() self._syncing = False self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS) - def _onGetRemoteClusterFailed(self): + def _onGetRemoteClusterFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None: self._syncing = False self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) @@ -132,19 +167,33 @@ class CloudOutputDeviceManager: """ new_devices = [] remote_clusters_added = False + host_guid_map = {machine.getMetaDataEntry(self.META_HOST_GUID): device_cluster_id + for device_cluster_id, machine in self._um_cloud_printers.items() + if machine.getMetaDataEntry(self.META_HOST_GUID)} + machine_manager = CuraApplication.getInstance().getMachineManager() + for cluster_data in clusters: device = CloudOutputDevice(self._api, cluster_data) - # Create a machine if we don't already have it. Do not make it the active machine. - machine_manager = CuraApplication.getInstance().getMachineManager() + # If the machine already existed before, it will be present in the host_guid_map + if cluster_data.host_guid in host_guid_map: + machine = machine_manager.getMachine(device.printerType, {self.META_HOST_GUID: cluster_data.host_guid}) + if machine and machine.getMetaDataEntry(self.META_CLUSTER_ID) != device.key: + # If the retrieved device has a different cluster_id than the existing machine, bring the existing + # machine up-to-date. + self._updateOutdatedMachine(outdated_machine = machine, new_cloud_output_device = device) + # Create a machine if we don't already have it. Do not make it the active machine. # We only need to add it if it wasn't already added by "local" network or by cloud. if machine_manager.getMachine(device.printerType, {self.META_CLUSTER_ID: device.key}) is None \ and machine_manager.getMachine(device.printerType, {self.META_NETWORK_KEY: cluster_data.host_name + "*"}) is None: # The host name is part of the network key. new_devices.append(device) - elif device.getId() not in self._remote_clusters: self._remote_clusters[device.getId()] = device remote_clusters_added = True + # If a printer that was removed from the account is re-added, change its metadata to mark it not removed + # from the account + elif not parseBool(self._um_cloud_printers[device.key].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")): + self._um_cloud_printers[device.key].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True) # Inform the Cloud printers model about new devices. new_devices_list_of_dicts = [{ @@ -160,7 +209,11 @@ class CloudOutputDeviceManager: self._connectToActiveMachine() return - new_devices.sort(key = lambda x: x.name.lower()) + # Sort new_devices on online status first, alphabetical second. + # Since the first device might be activated in case there is no active printer yet, + # it would be nice to prioritize online devices + online_cluster_names = {c.friendly_name.lower() for c in clusters if c.is_online and not c.friendly_name is None} + new_devices.sort(key = lambda x: ("a{}" if x.name.lower() in online_cluster_names else "b{}").format(x.name.lower())) image_path = os.path.join( CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or "", @@ -201,19 +254,122 @@ class CloudOutputDeviceManager: max_disp_devices = 3 if len(new_devices) > max_disp_devices: num_hidden = len(new_devices) - max_disp_devices + 1 - device_name_list = ["- {} ({})".format(device.name, device.printerTypeName) for device in new_devices[0:num_hidden]] - device_name_list.append(self.I18N_CATALOG.i18nc("info:hidden list items", "- and {} others", num_hidden)) - device_names = "\n".join(device_name_list) + device_name_list = ["
  • {} ({})
  • ".format(device.name, device.printerTypeName) for device in new_devices[0:num_hidden]] + device_name_list.append(self.I18N_CATALOG.i18nc("info:hidden list items", "
  • ... and {} others
  • ", num_hidden)) + device_names = "".join(device_name_list) else: - device_names = "\n".join(["- {} ({})".format(device.name, device.printerTypeName) for device in new_devices]) + device_names = "".join(["
  • {} ({})
  • ".format(device.name, device.printerTypeName) for device in new_devices]) message_text = self.I18N_CATALOG.i18nc( "info:status", - "Cloud printers added from your account:\n{}", + "Cloud printers added from your account:
      {}
    ", device_names ) message.setText(message_text) + def _updateOutdatedMachine(self, outdated_machine: GlobalStack, new_cloud_output_device: CloudOutputDevice) -> None: + """ + Update the cloud metadata of a pre-existing machine that is rediscovered (e.g. if the printer was removed and + re-added to the account) and delete the old CloudOutputDevice related to this machine. + + :param outdated_machine: The cloud machine that needs to be brought up-to-date with the new data received from + the account + :param new_cloud_output_device: The new CloudOutputDevice that should be linked to the pre-existing machine + :return: None + """ + old_cluster_id = outdated_machine.getMetaDataEntry(self.META_CLUSTER_ID) + outdated_machine.setMetaDataEntry(self.META_CLUSTER_ID, new_cloud_output_device.key) + outdated_machine.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True) + # Cleanup the remainings of the old CloudOutputDevice(old_cluster_id) + self._um_cloud_printers[new_cloud_output_device.key] = self._um_cloud_printers.pop(old_cluster_id) + output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() + if old_cluster_id in output_device_manager.getOutputDeviceIds(): + output_device_manager.removeOutputDevice(old_cluster_id) + if old_cluster_id in self._remote_clusters: + # We need to close the device so that it stops checking for its status + self._remote_clusters[old_cluster_id].close() + del self._remote_clusters[old_cluster_id] + self._remote_clusters[new_cloud_output_device.key] = new_cloud_output_device + + + def _devicesRemovedFromAccount(self, removed_device_ids: Set[str]) -> None: + """ + Removes the CloudOutputDevice from the received device ids and marks the specific printers as "removed from + account". In addition, it generates a message to inform the user about the printers that are no longer linked to + his/her account. The message is not generated if all the printers have been previously reported as not linked + to the account. + + :param removed_device_ids: Set of device ids, whose CloudOutputDevice needs to be removed + :return: None + """ + + if not CuraApplication.getInstance().getCuraAPI().account.isLoggedIn: + return + + # Do not report device ids which have been previously marked as non-linked to the account + ignored_device_ids = set() + for device_id in removed_device_ids: + if not parseBool(self._um_cloud_printers[device_id].getMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, "true")): + ignored_device_ids.add(device_id) + # Keep the reported_device_ids list in a class variable, so that the message button actions can access it and + # take the necessary steps to fulfill their purpose. + self.reported_device_ids = removed_device_ids - ignored_device_ids + if not self.reported_device_ids: + return + + # Generate message + self._removed_printers_message = Message( + title = self.I18N_CATALOG.i18ncp( + "info:status", + "Cloud connection is not available for a printer", + "Cloud connection is not available for some printers", + len(self.reported_device_ids) + ) + ) + device_names = "\n".join(["
  • {} ({})
  • ".format(self._um_cloud_printers[device].name, self._um_cloud_printers[device].definition.name) for device in self.reported_device_ids]) + message_text = self.I18N_CATALOG.i18ncp( + "info:status", + "The following cloud printer is not linked to your account:\n", + "The following cloud printers are not linked to your account:\n", + len(self.reported_device_ids) + ) + message_text += self.I18N_CATALOG.i18nc( + "info:status", + "
      {}
    \nTo establish a connection, please visit the " + "Ultimaker Digital Factory.", + device_names + ) + self._removed_printers_message.setText(message_text) + self._removed_printers_message.addAction("keep_printer_configurations_action", + name = self.I18N_CATALOG.i18nc("@action:button", "Keep printer configurations"), + icon = "", + description = "Keep the configuration of the cloud printer(s) synced with Cura which are not linked to your account.", + button_align = Message.ActionButtonAlignment.ALIGN_RIGHT) + self._removed_printers_message.addAction("remove_printers_action", + name = self.I18N_CATALOG.i18nc("@action:button", "Remove printers"), + icon = "", + description = "Remove the cloud printer(s) which are not linked to your account.", + button_style = Message.ActionButtonStyle.SECONDARY, + button_align = Message.ActionButtonAlignment.ALIGN_LEFT) + self._removed_printers_message.actionTriggered.connect(self._onRemovedPrintersMessageActionTriggered) + + output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() + + # Remove the output device from the printers + for device_id in removed_device_ids: + device = self._um_cloud_printers.get(device_id, None) # type: Optional[GlobalStack] + if not device: + continue + if device_id in output_device_manager.getOutputDeviceIds(): + output_device_manager.removeOutputDevice(device_id) + if device_id in self._remote_clusters: + del self._remote_clusters[device_id] + + # Update the printer's metadata to mark it as not linked to the account + device.setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, False) + + self._removed_printers_message.show() + def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: device = self._remote_clusters.pop(device_id, None) # type: Optional[CloudOutputDevice] if not device: @@ -264,7 +420,16 @@ class CloudOutputDeviceManager: def _setOutputDeviceMetadata(self, device: CloudOutputDevice, machine: GlobalStack): machine.setName(device.name) machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) + machine.setMetaDataEntry(self.META_HOST_GUID, device.clusterData.host_guid) machine.setMetaDataEntry("group_name", device.name) + machine.setMetaDataEntry("group_size", device.clusterSize) + machine.setMetaDataEntry("removal_warning", self.I18N_CATALOG.i18nc( + "@label ({} is printer name)", + "{} will be removed until the next account sync.
    To remove {} permanently, " + "visit Ultimaker Digital Factory. " + "

    Are you sure you want to remove {} temporarily?", + device.name, device.name, device.name + )) machine.addConfiguredConnectionType(device.connectionType.value) def _connectToOutputDevice(self, device: CloudOutputDevice, machine: GlobalStack) -> None: @@ -277,4 +442,38 @@ class CloudOutputDeviceManager: output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() if device.key not in output_device_manager.getOutputDeviceIds(): - output_device_manager.addOutputDevice(device) \ No newline at end of file + output_device_manager.addOutputDevice(device) + + def _printerRemoved(self, container: ContainerInterface) -> None: + """ + Callback connected to the containerRemoved signal. Invoked when a cloud printer is removed from Cura to remove + the printer's reference from the _remote_clusters. + + :param container: The ContainerInterface passed to this function whenever the ContainerRemoved signal is emitted + :return: None + """ + if isinstance(container, GlobalStack): + container_cluster_id = container.getMetaDataEntry(self.META_CLUSTER_ID, None) + if container_cluster_id in self._remote_clusters.keys(): + del self._remote_clusters[container_cluster_id] + + def _onRemovedPrintersMessageActionTriggered(self, removed_printers_message: Message, action: str) -> None: + if action == "keep_printer_configurations_action": + removed_printers_message.hide() + elif action == "remove_printers_action": + machine_manager = CuraApplication.getInstance().getMachineManager() + remove_printers_ids = {self._um_cloud_printers[i].getId() for i in self.reported_device_ids} + all_ids = {m.getId() for m in CuraApplication.getInstance().getContainerRegistry().findContainerStacks(type = "machine")} + + question_title = self.I18N_CATALOG.i18nc("@title:window", "Remove printers?") + question_content = self.I18N_CATALOG.i18nc("@label", "You are about to remove {} printer(s) from Cura. This action cannot be undone. \nAre you sure you want to continue?".format(len(remove_printers_ids))) + if remove_printers_ids == all_ids: + question_content = self.I18N_CATALOG.i18nc("@label", "You are about to remove all printers from Cura. This action cannot be undone. \nAre you sure you want to continue?") + result = QMessageBox.question(None, question_title, question_content) + if result == QMessageBox.No: + return + + for machine_cloud_id in self.reported_device_ids: + machine_manager.setActiveMachine(self._um_cloud_printers[machine_cloud_id].getId()) + machine_manager.removeMachine(self._um_cloud_printers[machine_cloud_id].getId()) + removed_printers_message.hide() diff --git a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py index 6aa341c0e5..3c80565fa1 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py @@ -1,17 +1,18 @@ # Copyright (c) 2019 Ultimaker B.V. # !/usr/bin/env python # -*- coding: utf-8 -*- -from PyQt5.QtCore import QUrl -from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager -from typing import Optional, Callable, Any, Tuple, cast +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply +from typing import Callable, Any, Tuple, cast, Dict, Optional from UM.Logger import Logger +from UM.TaskManagement.HttpRequestManager import HttpRequestManager from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse -## Class responsible for uploading meshes to the cloud in separate requests. class ToolPathUploader: + """Class responsible for uploading meshes to the cloud in separate requests.""" + # The maximum amount of times to retry if the server returns one of the RETRY_HTTP_CODES MAX_RETRIES = 10 @@ -19,20 +20,20 @@ class ToolPathUploader: # The HTTP codes that should trigger a retry. RETRY_HTTP_CODES = {500, 502, 503, 504} - # The amount of bytes to send per request - BYTES_PER_REQUEST = 256 * 1024 - - ## Creates a mesh upload object. - # \param manager: The network access manager that will handle the HTTP requests. - # \param print_job: The print job response that was returned by the cloud after registering the upload. - # \param data: The mesh bytes to be uploaded. - # \param on_finished: The method to be called when done. - # \param on_progress: The method to be called when the progress changes (receives a percentage 0-100). - # \param on_error: The method to be called when an error occurs. - def __init__(self, manager: QNetworkAccessManager, print_job: CloudPrintJobResponse, data: bytes, + def __init__(self, http: HttpRequestManager, print_job: CloudPrintJobResponse, data: bytes, on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any] ) -> None: - self._manager = manager + """Creates a mesh upload object. + + :param manager: The network access manager that will handle the HTTP requests. + :param print_job: The print job response that was returned by the cloud after registering the upload. + :param data: The mesh bytes to be uploaded. + :param on_finished: The method to be called when done. + :param on_progress: The method to be called when the progress changes (receives a percentage 0-100). + :param on_error: The method to be called when an error occurs. + """ + + self._http = http self._print_job = print_job self._data = data @@ -40,82 +41,70 @@ class ToolPathUploader: self._on_progress = on_progress self._on_error = on_error - self._sent_bytes = 0 self._retries = 0 self._finished = False - self._reply = None # type: Optional[QNetworkReply] - ## Returns the print job for which this object was created. @property def printJob(self): + """Returns the print job for which this object was created.""" + return self._print_job - ## Creates a network request to the print job upload URL, adding the needed content range header. - def _createRequest(self) -> QNetworkRequest: - request = QNetworkRequest(QUrl(self._print_job.upload_url)) - request.setHeader(QNetworkRequest.ContentTypeHeader, self._print_job.content_type) - - first_byte, last_byte = self._chunkRange() - content_range = "bytes {}-{}/{}".format(first_byte, last_byte - 1, len(self._data)) - request.setRawHeader(b"Content-Range", content_range.encode()) - Logger.log("i", "Uploading %s to %s", content_range, self._print_job.upload_url) - - return request - - ## Determines the bytes that should be uploaded next. - # \return: A tuple with the first and the last byte to upload. - def _chunkRange(self) -> Tuple[int, int]: - last_byte = min(len(self._data), self._sent_bytes + self.BYTES_PER_REQUEST) - return self._sent_bytes, last_byte - - ## Starts uploading the mesh. def start(self) -> None: + """Starts uploading the mesh.""" + if self._finished: # reset state. - self._sent_bytes = 0 self._retries = 0 self._finished = False - self._uploadChunk() + self._upload() - ## Stops uploading the mesh, marking it as finished. def stop(self): - Logger.log("i", "Stopped uploading") - self._finished = True + """Stops uploading the mesh, marking it as finished.""" - ## Uploads a chunk of the mesh to the cloud. - def _uploadChunk(self) -> None: + Logger.log("i", "Finished uploading") + self._finished = True # Signal to any ongoing retries that we should stop retrying. + self._on_finished() + + def _upload(self) -> None: + """ + Uploads the print job to the cloud printer. + """ if self._finished: raise ValueError("The upload is already finished") - first_byte, last_byte = self._chunkRange() - request = self._createRequest() + Logger.log("i", "Uploading print to {upload_url}".format(upload_url = self._print_job.upload_url)) + self._http.put( + url = cast(str, self._print_job.upload_url), + headers_dict = {"Content-Type": cast(str, self._print_job.content_type)}, + data = self._data, + callback = self._finishedCallback, + error_callback = self._errorCallback, + upload_progress_callback = self._progressCallback + ) - # now send the reply and subscribe to the results - self._reply = self._manager.put(request, self._data[first_byte:last_byte]) - self._reply.finished.connect(self._finishedCallback) - self._reply.uploadProgress.connect(self._progressCallback) - self._reply.error.connect(self._errorCallback) - - ## Handles an update to the upload progress - # \param bytes_sent: The amount of bytes sent in the current request. - # \param bytes_total: The amount of bytes to send in the current request. def _progressCallback(self, bytes_sent: int, bytes_total: int) -> None: - Logger.log("i", "Progress callback %s / %s", bytes_sent, bytes_total) + """Handles an update to the upload progress + + :param bytes_sent: The amount of bytes sent in the current request. + :param bytes_total: The amount of bytes to send in the current request. + """ + Logger.debug("Cloud upload progress %s / %s", bytes_sent, bytes_total) if bytes_total: - total_sent = self._sent_bytes + bytes_sent - self._on_progress(int(total_sent / len(self._data) * 100)) + self._on_progress(int(bytes_sent / len(self._data) * 100)) ## Handles an error uploading. - def _errorCallback(self) -> None: - reply = cast(QNetworkReply, self._reply) + def _errorCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None: + """Handles an error uploading.""" + body = bytes(reply.readAll()).decode() Logger.log("e", "Received error while uploading: %s", body) self.stop() self._on_error() - ## Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed. - def _finishedCallback(self) -> None: - reply = cast(QNetworkReply, self._reply) + def _finishedCallback(self, reply: QNetworkReply) -> None: + """Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed.""" + Logger.log("i", "Finished callback %s %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString()) @@ -126,27 +115,17 @@ class ToolPathUploader: self._retries += 1 Logger.log("i", "Retrying %s/%s request %s", self._retries, self.MAX_RETRIES, reply.url().toString()) try: - self._uploadChunk() + self._upload() except ValueError: # Asynchronously it could have completed in the meanwhile. pass return # Http codes that are not to be retried are assumed to be errors. if status_code > 308: - self._errorCallback() + self._errorCallback(reply, None) return Logger.log("d", "status_code: %s, Headers: %s, body: %s", status_code, [bytes(header).decode() for header in reply.rawHeaderList()], bytes(reply.readAll()).decode()) - self._chunkUploaded() - ## Handles a chunk of data being uploaded, starting the next chunk if needed. - def _chunkUploaded(self) -> None: - # We got a successful response. Let's start the next chunk or report the upload is finished. - first_byte, last_byte = self._chunkRange() - self._sent_bytes += last_byte - first_byte - if self._sent_bytes >= len(self._data): - self.stop() - self._on_finished() - else: - self._uploadChunk() + self.stop() diff --git a/plugins/UM3NetworkPrinting/src/ExportFileJob.py b/plugins/UM3NetworkPrinting/src/ExportFileJob.py index 56d15bc835..6fde08cc5f 100644 --- a/plugins/UM3NetworkPrinting/src/ExportFileJob.py +++ b/plugins/UM3NetworkPrinting/src/ExportFileJob.py @@ -9,8 +9,8 @@ from cura.CuraApplication import CuraApplication from .MeshFormatHandler import MeshFormatHandler -## Job that exports the build plate to the correct file format for the target cluster. class ExportFileJob(WriteFileJob): + """Job that exports the build plate to the correct file format for the target cluster.""" def __init__(self, file_handler: Optional[FileHandler], nodes: List[SceneNode], firmware_version: str) -> None: @@ -27,12 +27,14 @@ class ExportFileJob(WriteFileJob): extension = self._mesh_format_handler.preferred_format.get("extension", "") self.setFileName("{}.{}".format(job_name, extension)) - ## Get the mime type of the selected export file type. def getMimeType(self) -> str: + """Get the mime type of the selected export file type.""" + return self._mesh_format_handler.mime_type - ## Get the job result as bytes as that is what we need to upload to the cluster. def getOutput(self) -> bytes: + """Get the job result as bytes as that is what we need to upload to the cluster.""" + output = self.getStream().getvalue() if isinstance(output, str): output = output.encode("utf-8") diff --git a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py index 9927bf744e..7fc1b4a7d3 100644 --- a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py +++ b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py @@ -16,8 +16,9 @@ from cura.CuraApplication import CuraApplication I18N_CATALOG = i18nCatalog("cura") -## This class is responsible for choosing the formats used by the connected clusters. class MeshFormatHandler: + """This class is responsible for choosing the formats used by the connected clusters.""" + def __init__(self, file_handler: Optional[FileHandler], firmware_version: str) -> None: self._file_handler = file_handler or CuraApplication.getInstance().getMeshFileHandler() @@ -28,42 +29,50 @@ class MeshFormatHandler: def is_valid(self) -> bool: return bool(self._writer) - ## Chooses the preferred file format. - # \return A dict with the file format details, with the following keys: - # {id: str, extension: str, description: str, mime_type: str, mode: int, hide_in_file_dialog: bool} @property def preferred_format(self) -> Dict[str, Union[str, int, bool]]: + """Chooses the preferred file format. + + :return: A dict with the file format details, with the following keys: + {id: str, extension: str, description: str, mime_type: str, mode: int, hide_in_file_dialog: bool} + """ return self._preferred_format - ## Gets the file writer for the given file handler and mime type. - # \return A file writer. @property def writer(self) -> Optional[FileWriter]: + """Gets the file writer for the given file handler and mime type. + + :return: A file writer. + """ return self._writer @property def mime_type(self) -> str: return cast(str, self._preferred_format["mime_type"]) - ## Gets the file mode (FileWriter.OutputMode.TextMode or FileWriter.OutputMode.BinaryMode) @property def file_mode(self) -> int: + """Gets the file mode (FileWriter.OutputMode.TextMode or FileWriter.OutputMode.BinaryMode)""" + return cast(int, self._preferred_format["mode"]) - ## Gets the file extension @property def file_extension(self) -> str: + """Gets the file extension""" + return cast(str, self._preferred_format["extension"]) - ## Creates the right kind of stream based on the preferred format. def createStream(self) -> Union[io.BytesIO, io.StringIO]: + """Creates the right kind of stream based on the preferred format.""" + if self.file_mode == FileWriter.OutputMode.TextMode: return io.StringIO() else: return io.BytesIO() - ## Writes the mesh and returns its value. def getBytes(self, nodes: List[SceneNode]) -> bytes: + """Writes the mesh and returns its value.""" + if self.writer is None: raise ValueError("There is no writer for the mesh format handler.") stream = self.createStream() @@ -73,10 +82,12 @@ class MeshFormatHandler: value = value.encode() return value - ## Chooses the preferred file format for the given file handler. - # \param firmware_version: The version of the firmware. - # \return A dict with the file format details. def _getPreferredFormat(self, firmware_version: str) -> Dict[str, Union[str, int, bool]]: + """Chooses the preferred file format for the given file handler. + + :param firmware_version: The version of the firmware. + :return: A dict with the file format details. + """ # Formats supported by this application (file types that we can actually write). application = CuraApplication.getInstance() @@ -108,9 +119,11 @@ class MeshFormatHandler: ) return file_formats[0] - ## Gets the file writer for the given file handler and mime type. - # \param mime_type: The mine type. - # \return A file writer. def _getWriter(self, mime_type: str) -> Optional[FileWriter]: + """Gets the file writer for the given file handler and mime type. + + :param mime_type: The mine type. + :return: A file writer. + """ # Just take the first file format available. return self._file_handler.getWriterByMimeType(mime_type) diff --git a/plugins/UM3NetworkPrinting/src/Messages/LegacyDeviceNoLongerSupportedMessage.py b/plugins/UM3NetworkPrinting/src/Messages/LegacyDeviceNoLongerSupportedMessage.py index f4132dbcbc..146767467a 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/LegacyDeviceNoLongerSupportedMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/LegacyDeviceNoLongerSupportedMessage.py @@ -7,12 +7,12 @@ from UM.Message import Message I18N_CATALOG = i18nCatalog("cura") -## Message shown when trying to connect to a legacy printer device. class LegacyDeviceNoLongerSupportedMessage(Message): - - # Singleton used to prevent duplicate messages of this type at the same time. + """Message shown when trying to connect to a legacy printer device.""" + __is_visible = False - + """Singleton used to prevent duplicate messages of this type at the same time.""" + def __init__(self) -> None: super().__init__( text = I18N_CATALOG.i18nc("@info:status", "You are attempting to connect to a printer that is not " diff --git a/plugins/UM3NetworkPrinting/src/Messages/MaterialSyncMessage.py b/plugins/UM3NetworkPrinting/src/Messages/MaterialSyncMessage.py index e021b2ae99..6b481ff4a1 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/MaterialSyncMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/MaterialSyncMessage.py @@ -13,11 +13,11 @@ if TYPE_CHECKING: I18N_CATALOG = i18nCatalog("cura") -## Message shown when sending material files to cluster host. class MaterialSyncMessage(Message): + """Message shown when sending material files to cluster host.""" - # Singleton used to prevent duplicate messages of this type at the same time. __is_visible = False + """Singleton used to prevent duplicate messages of this type at the same time.""" def __init__(self, device: "UltimakerNetworkedPrinterOutputDevice") -> None: super().__init__( diff --git a/plugins/UM3NetworkPrinting/src/Messages/NotClusterHostMessage.py b/plugins/UM3NetworkPrinting/src/Messages/NotClusterHostMessage.py index 77d7995fc7..70bfa769ee 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/NotClusterHostMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/NotClusterHostMessage.py @@ -16,11 +16,11 @@ if TYPE_CHECKING: I18N_CATALOG = i18nCatalog("cura") -## Message shown when trying to connect to a printer that is not a host. class NotClusterHostMessage(Message): + """Message shown when trying to connect to a printer that is not a host.""" - # Singleton used to prevent duplicate messages of this type at the same time. __is_visible = False + """Singleton used to prevent duplicate messages of this type at the same time.""" def __init__(self, device: "UltimakerNetworkedPrinterOutputDevice") -> None: super().__init__( diff --git a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadBlockedMessage.py b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadBlockedMessage.py index be00292559..39dc985cb8 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadBlockedMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadBlockedMessage.py @@ -7,9 +7,9 @@ from UM.Message import Message I18N_CATALOG = i18nCatalog("cura") -## Message shown when uploading a print job to a cluster is blocked because another upload is already in progress. class PrintJobUploadBlockedMessage(Message): - + """Message shown when uploading a print job to a cluster is blocked because another upload is already in progress.""" + def __init__(self) -> None: super().__init__( text = I18N_CATALOG.i18nc("@info:status", "Please wait until the current job has been sent."), diff --git a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadErrorMessage.py b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadErrorMessage.py index bb26a84953..5145844ea7 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadErrorMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadErrorMessage.py @@ -7,9 +7,9 @@ from UM.Message import Message I18N_CATALOG = i18nCatalog("cura") -## Message shown when uploading a print job to a cluster failed. class PrintJobUploadErrorMessage(Message): - + """Message shown when uploading a print job to a cluster failed.""" + def __init__(self, message: str = None) -> None: super().__init__( text = message or I18N_CATALOG.i18nc("@info:text", "Could not upload the data to the printer."), diff --git a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadProgressMessage.py b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadProgressMessage.py index bdbab008e3..63fa037890 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadProgressMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadProgressMessage.py @@ -7,8 +7,9 @@ from UM.Message import Message I18N_CATALOG = i18nCatalog("cura") -## Class responsible for showing a progress message while a mesh is being uploaded to the cloud. class PrintJobUploadProgressMessage(Message): + """Class responsible for showing a progress message while a mesh is being uploaded to the cloud.""" + def __init__(self): super().__init__( title = I18N_CATALOG.i18nc("@info:status", "Sending Print Job"), @@ -19,14 +20,17 @@ class PrintJobUploadProgressMessage(Message): use_inactivity_timer = False ) - ## Shows the progress message. def show(self): + """Shows the progress message.""" + self.setProgress(0) super().show() - ## Updates the percentage of the uploaded. - # \param percentage: The percentage amount (0-100). def update(self, percentage: int) -> None: + """Updates the percentage of the uploaded. + + :param percentage: The percentage amount (0-100). + """ if not self._visible: super().show() self.setProgress(percentage) diff --git a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadSuccessMessage.py b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadSuccessMessage.py index c9be28d57f..aa64f338dd 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadSuccessMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadSuccessMessage.py @@ -7,9 +7,9 @@ from UM.Message import Message I18N_CATALOG = i18nCatalog("cura") -## Message shown when uploading a print job to a cluster succeeded. class PrintJobUploadSuccessMessage(Message): - + """Message shown when uploading a print job to a cluster succeeded.""" + def __init__(self) -> None: super().__init__( text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."), diff --git a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py index 3d38a4b116..92d7246489 100644 --- a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py @@ -18,45 +18,56 @@ class BaseModel: def validate(self) -> None: pass - ## Checks whether the two models are equal. - # \param other: The other model. - # \return True if they are equal, False if they are different. def __eq__(self, other): + """Checks whether the two models are equal. + + :param other: The other model. + :return: True if they are equal, False if they are different. + """ return type(self) == type(other) and self.toDict() == other.toDict() - ## Checks whether the two models are different. - # \param other: The other model. - # \return True if they are different, False if they are the same. def __ne__(self, other) -> bool: + """Checks whether the two models are different. + + :param other: The other model. + :return: True if they are different, False if they are the same. + """ return type(self) != type(other) or self.toDict() != other.toDict() - ## Converts the model into a serializable dictionary def toDict(self) -> Dict[str, Any]: + """Converts the model into a serializable dictionary""" + return self.__dict__ - ## Parses a single model. - # \param model_class: The model class. - # \param values: The value of the model, which is usually a dictionary, but may also be already parsed. - # \return An instance of the model_class given. @staticmethod def parseModel(model_class: Type[T], values: Union[T, Dict[str, Any]]) -> T: + """Parses a single model. + + :param model_class: The model class. + :param values: The value of the model, which is usually a dictionary, but may also be already parsed. + :return: An instance of the model_class given. + """ if isinstance(values, dict): return model_class(**values) return values - ## Parses a list of models. - # \param model_class: The model class. - # \param values: The value of the list. Each value is usually a dictionary, but may also be already parsed. - # \return A list of instances of the model_class given. @classmethod def parseModels(cls, model_class: Type[T], values: List[Union[T, Dict[str, Any]]]) -> List[T]: + """Parses a list of models. + + :param model_class: The model class. + :param values: The value of the list. Each value is usually a dictionary, but may also be already parsed. + :return: A list of instances of the model_class given. + """ return [cls.parseModel(model_class, value) for value in values] - ## Parses the given date string. - # \param date: The date to parse. - # \return The parsed date. @staticmethod def parseDate(date: Union[str, datetime]) -> datetime: + """Parses the given date string. + + :param date: The date to parse. + :return: The parsed date. + """ if isinstance(date, datetime): return date return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py index 7ecfe8b0a3..a9107db3c8 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py @@ -5,22 +5,27 @@ from typing import Optional from ..BaseModel import BaseModel -## Class representing a cloud connected cluster. class CloudClusterResponse(BaseModel): + """Class representing a cloud connected cluster.""" + - ## Creates a new cluster response object. - # \param cluster_id: The secret unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='. - # \param host_guid: The unique identifier of the print cluster host, e.g. 'e90ae0ac-1257-4403-91ee-a44c9b7e8050'. - # \param host_name: The name of the printer as configured during the Wi-Fi setup. Used as identifier for end users. - # \param is_online: Whether this cluster is currently connected to the cloud. - # \param status: The status of the cluster authentication (active or inactive). - # \param host_version: The firmware version of the cluster host. This is where the Stardust client is running on. - # \param host_internal_ip: The internal IP address of the host printer. - # \param friendly_name: The human readable name of the host printer. - # \param printer_type: The machine type of the host printer. def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str, host_internal_ip: Optional[str] = None, host_version: Optional[str] = None, - friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", **kwargs) -> None: + friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", printer_count: int = 1, **kwargs) -> None: + """Creates a new cluster response object. + + :param cluster_id: The secret unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='. + :param host_guid: The unique identifier of the print cluster host, e.g. 'e90ae0ac-1257-4403-91ee-a44c9b7e8050'. + :param host_name: The name of the printer as configured during the Wi-Fi setup. Used as identifier for end users. + :param is_online: Whether this cluster is currently connected to the cloud. + :param status: The status of the cluster authentication (active or inactive). + :param host_version: The firmware version of the cluster host. This is where the Stardust client is running on. + :param host_internal_ip: The internal IP address of the host printer. + :param friendly_name: The human readable name of the host printer. + :param printer_type: The machine type of the host printer. + :param printer_count: The amount of printers in the print cluster. 1 for a single printer + """ + self.cluster_id = cluster_id self.host_guid = host_guid self.host_name = host_name @@ -30,6 +35,7 @@ class CloudClusterResponse(BaseModel): self.host_internal_ip = host_internal_ip self.friendly_name = friendly_name self.printer_type = printer_type + self.printer_count = printer_count super().__init__(**kwargs) # Validates the model, raising an exception if the model is invalid. diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py index 330e61d343..5cd151d8ef 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py @@ -11,15 +11,17 @@ from .ClusterPrintJobStatus import ClusterPrintJobStatus # Model that represents the status of the cluster for the cloud class CloudClusterStatus(BaseModel): - ## Creates a new cluster status model object. - # \param printers: The latest status of each printer in the cluster. - # \param print_jobs: The latest status of each print job in the cluster. - # \param generated_time: The datetime when the object was generated on the server-side. - def __init__(self, - printers: List[Union[ClusterPrinterStatus, Dict[str, Any]]], + def __init__(self, printers: List[Union[ClusterPrinterStatus, Dict[str, Any]]], print_jobs: List[Union[ClusterPrintJobStatus, Dict[str, Any]]], generated_time: Union[str, datetime], **kwargs) -> None: + """Creates a new cluster status model object. + + :param printers: The latest status of each printer in the cluster. + :param print_jobs: The latest status of each print job in the cluster. + :param generated_time: The datetime when the object was generated on the server-side. + """ + self.generated_time = self.parseDate(generated_time) self.printers = self.parseModels(ClusterPrinterStatus, printers) self.print_jobs = self.parseModels(ClusterPrintJobStatus, print_jobs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py index 9381e4b8cf..05f303e5c9 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py @@ -5,20 +5,23 @@ from typing import Dict, Optional, Any from ..BaseModel import BaseModel -## Class representing errors generated by the cloud servers, according to the JSON-API standard. class CloudError(BaseModel): + """Class representing errors generated by the cloud servers, according to the JSON-API standard.""" - ## Creates a new error object. - # \param id: Unique identifier for this particular occurrence of the problem. - # \param title: A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence - # of the problem, except for purposes of localization. - # \param code: An application-specific error code, expressed as a string value. - # \param detail: A human-readable explanation specific to this occurrence of the problem. Like title, this field's - # value can be localized. - # \param http_status: The HTTP status code applicable to this problem, converted to string. - # \param meta: Non-standard meta-information about the error, depending on the error code. def __init__(self, id: str, code: str, title: str, http_status: str, detail: Optional[str] = None, meta: Optional[Dict[str, Any]] = None, **kwargs) -> None: + """Creates a new error object. + + :param id: Unique identifier for this particular occurrence of the problem. + :param title: A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence + of the problem, except for purposes of localization. + :param code: An application-specific error code, expressed as a string value. + :param detail: A human-readable explanation specific to this occurrence of the problem. Like title, this field's + value can be localized. + :param http_status: The HTTP status code applicable to this problem, converted to string. + :param meta: Non-standard meta-information about the error, depending on the error code. + """ + self.id = id self.code = code self.http_status = http_status diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py index a1880e8751..83cbb5a030 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py @@ -8,19 +8,22 @@ from ..BaseModel import BaseModel # Model that represents the response received from the cloud after requesting to upload a print job class CloudPrintJobResponse(BaseModel): - ## Creates a new print job response model. - # \param job_id: The job unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='. - # \param status: The status of the print job. - # \param status_description: Contains more details about the status, e.g. the cause of failures. - # \param download_url: A signed URL to download the resulting status. Only available when the job is finished. - # \param job_name: The name of the print job. - # \param slicing_details: Model for slice information. - # \param upload_url: The one-time use URL where the toolpath must be uploaded to (only if status is uploading). - # \param content_type: The content type of the print job (e.g. text/plain or application/gzip) - # \param generated_time: The datetime when the object was generated on the server-side. def __init__(self, job_id: str, status: str, download_url: Optional[str] = None, job_name: Optional[str] = None, upload_url: Optional[str] = None, content_type: Optional[str] = None, status_description: Optional[str] = None, slicing_details: Optional[dict] = None, **kwargs) -> None: + """Creates a new print job response model. + + :param job_id: The job unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='. + :param status: The status of the print job. + :param status_description: Contains more details about the status, e.g. the cause of failures. + :param download_url: A signed URL to download the resulting status. Only available when the job is finished. + :param job_name: The name of the print job. + :param slicing_details: Model for slice information. + :param upload_url: The one-time use URL where the toolpath must be uploaded to (only if status is uploading). + :param content_type: The content type of the print job (e.g. text/plain or application/gzip) + :param generated_time: The datetime when the object was generated on the server-side. + """ + self.job_id = job_id self.status = status self.download_url = download_url diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py index ff705ae495..7e108027d9 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py @@ -6,11 +6,14 @@ from ..BaseModel import BaseModel # Model that represents the request to upload a print job to the cloud class CloudPrintJobUploadRequest(BaseModel): - ## Creates a new print job upload request. - # \param job_name: The name of the print job. - # \param file_size: The size of the file in bytes. - # \param content_type: The content type of the print job (e.g. text/plain or application/gzip) def __init__(self, job_name: str, file_size: int, content_type: str, **kwargs) -> None: + """Creates a new print job upload request. + + :param job_name: The name of the print job. + :param file_size: The size of the file in bytes. + :param content_type: The content type of the print job (e.g. text/plain or application/gzip) + """ + self.job_name = job_name self.file_size = file_size self.content_type = content_type diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py index b108f40e27..f8b3210210 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py @@ -9,13 +9,16 @@ from ..BaseModel import BaseModel # Model that represents the responses received from the cloud after requesting a job to be printed. class CloudPrintResponse(BaseModel): - ## Creates a new print response object. - # \param job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect. - # \param status: The status of the print request (queued or failed). - # \param generated_time: The datetime when the object was generated on the server-side. - # \param cluster_job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect. def __init__(self, job_id: str, status: str, generated_time: Union[str, datetime], cluster_job_id: Optional[str] = None, **kwargs) -> None: + """Creates a new print response object. + + :param job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect. + :param status: The status of the print request (queued or failed). + :param generated_time: The datetime when the object was generated on the server-side. + :param cluster_job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect. + """ + self.job_id = job_id self.status = status self.cluster_job_id = cluster_job_id diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py index a5a392488d..771389e102 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py @@ -3,11 +3,13 @@ from ..BaseModel import BaseModel -## Class representing a cluster printer class ClusterBuildPlate(BaseModel): + """Class representing a cluster printer""" - ## Create a new build plate - # \param type: The type of build plate glass or aluminium def __init__(self, type: str = "glass", **kwargs) -> None: + """Create a new build plate + + :param type: The type of build plate glass or aluminium + """ self.type = type super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py index e11d2be2d2..529f3928fd 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py @@ -9,26 +9,33 @@ from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMate from ..BaseModel import BaseModel -## Class representing a cloud cluster printer configuration -# Also used for representing slots in a Material Station (as from Cura's perspective these are the same). class ClusterPrintCoreConfiguration(BaseModel): + """Class representing a cloud cluster printer configuration + + Also used for representing slots in a Material Station (as from Cura's perspective these are the same). + """ + + def __init__(self, extruder_index: int, material: Union[None, Dict[str, Any], + ClusterPrinterConfigurationMaterial] = None, print_core_id: Optional[str] = None, **kwargs) -> None: + """Creates a new cloud cluster printer configuration object + + :param extruder_index: The position of the extruder on the machine as list index. Numbered from left to right. + :param material: The material of a configuration object in a cluster printer. May be in a dict or an object. + :param nozzle_diameter: The diameter of the print core at this position in millimeters, e.g. '0.4'. + :param print_core_id: The type of print core inserted at this position, e.g. 'AA 0.4'. + """ - ## Creates a new cloud cluster printer configuration object - # \param extruder_index: The position of the extruder on the machine as list index. Numbered from left to right. - # \param material: The material of a configuration object in a cluster printer. May be in a dict or an object. - # \param nozzle_diameter: The diameter of the print core at this position in millimeters, e.g. '0.4'. - # \param print_core_id: The type of print core inserted at this position, e.g. 'AA 0.4'. - def __init__(self, extruder_index: int, - material: Union[None, Dict[str, Any], ClusterPrinterConfigurationMaterial] = None, - print_core_id: Optional[str] = None, **kwargs) -> None: self.extruder_index = extruder_index self.material = self.parseModel(ClusterPrinterConfigurationMaterial, material) if material else None self.print_core_id = print_core_id super().__init__(**kwargs) - ## Updates the given output model. - # \param model - The output model to update. def updateOutputModel(self, model: ExtruderOutputModel) -> None: + """Updates the given output model. + + :param model: The output model to update. + """ + if self.print_core_id is not None: model.updateHotendID(self.print_core_id) @@ -40,14 +47,16 @@ class ClusterPrintCoreConfiguration(BaseModel): else: model.updateActiveMaterial(None) - ## Creates a configuration model def createConfigurationModel(self) -> ExtruderConfigurationModel: + """Creates a configuration model""" + model = ExtruderConfigurationModel(position = self.extruder_index) self.updateConfigurationModel(model) return model - ## Creates a configuration model def updateConfigurationModel(self, model: ExtruderConfigurationModel) -> ExtruderConfigurationModel: + """Creates a configuration model""" + model.setHotendID(self.print_core_id) if self.material: model.setMaterial(self.material.createOutputModel()) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py index 88251bbf53..0c83cd1b31 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py @@ -5,19 +5,22 @@ from typing import Optional from ..BaseModel import BaseModel -## Model for the types of changes that are needed before a print job can start class ClusterPrintJobConfigurationChange(BaseModel): + """Model for the types of changes that are needed before a print job can start""" + + + def __init__(self, type_of_change: str, target_id: str, origin_id: str, index: Optional[int] = None, + target_name: Optional[str] = None, origin_name: Optional[str] = None, **kwargs) -> None: + """Creates a new print job constraint. + + :param type_of_change: The type of configuration change, one of: "material", "print_core_change" + :param index: The hotend slot or extruder index to change + :param target_id: Target material guid or hotend id + :param origin_id: Original/current material guid or hotend id + :param target_name: Target material name or hotend id + :param origin_name: Original/current material name or hotend id + """ - ## Creates a new print job constraint. - # \param type_of_change: The type of configuration change, one of: "material", "print_core_change" - # \param index: The hotend slot or extruder index to change - # \param target_id: Target material guid or hotend id - # \param origin_id: Original/current material guid or hotend id - # \param target_name: Target material name or hotend id - # \param origin_name: Original/current material name or hotend id - def __init__(self, type_of_change: str, target_id: str, origin_id: str, - index: Optional[int] = None, target_name: Optional[str] = None, origin_name: Optional[str] = None, - **kwargs) -> None: self.type_of_change = type_of_change self.index = index self.target_id = target_id diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py index 9239004b18..5271130dd6 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py @@ -5,12 +5,14 @@ from typing import Optional from ..BaseModel import BaseModel -## Class representing a cloud cluster print job constraint class ClusterPrintJobConstraints(BaseModel): + """Class representing a cloud cluster print job constraint""" - ## Creates a new print job constraint. - # \param require_printer_name: Unique name of the printer that this job should be printed on. - # Should be one of the unique_name field values in the cluster, e.g. 'ultimakersystem-ccbdd30044ec' def __init__(self, require_printer_name: Optional[str] = None, **kwargs) -> None: + """Creates a new print job constraint. + + :param require_printer_name: Unique name of the printer that this job should be printed on. + Should be one of the unique_name field values in the cluster, e.g. 'ultimakersystem-ccbdd30044ec' + """ self.require_printer_name = require_printer_name super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py index 5a8f0aa46d..3c9e03223a 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py @@ -3,14 +3,17 @@ from ..BaseModel import BaseModel -## Class representing the reasons that prevent this job from being printed on the associated printer class ClusterPrintJobImpediment(BaseModel): + """Class representing the reasons that prevent this job from being printed on the associated printer""" - ## Creates a new print job constraint. - # \param translation_key: A string indicating a reason the print cannot be printed, - # such as 'does_not_fit_in_build_volume' - # \param severity: A number indicating the severity of the problem, with higher being more severe def __init__(self, translation_key: str, severity: int, **kwargs) -> None: + """Creates a new print job constraint. + + :param translation_key: A string indicating a reason the print cannot be printed, + such as 'does_not_fit_in_build_volume' + :param severity: A number indicating the severity of the problem, with higher being more severe + """ + self.translation_key = translation_key self.severity = severity super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py index 22fb9bb37a..6e46c12cf0 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py @@ -15,36 +15,9 @@ from ..BaseModel import BaseModel from ...ClusterOutputController import ClusterOutputController -## Model for the status of a single print job in a cluster. class ClusterPrintJobStatus(BaseModel): + """Model for the status of a single print job in a cluster.""" - ## Creates a new cloud print job status model. - # \param assigned_to: The name of the printer this job is assigned to while being queued. - # \param configuration: The required print core configurations of this print job. - # \param constraints: Print job constraints object. - # \param created_at: The timestamp when the job was created in Cura Connect. - # \param force: Allow this job to be printed despite of mismatching configurations. - # \param last_seen: The number of seconds since this job was checked. - # \param machine_variant: The machine type that this job should be printed on.Coincides with the machine_type field - # of the printer object. - # \param name: The name of the print job. Usually the name of the .gcode file. - # \param network_error_count: The number of errors encountered when requesting data for this print job. - # \param owner: The name of the user who added the print job to Cura Connect. - # \param printer_uuid: UUID of the printer that the job is currently printing on or assigned to. - # \param started: Whether the job has started printing or not. - # \param status: The status of the print job. - # \param time_elapsed: The remaining printing time in seconds. - # \param time_total: The total printing time in seconds. - # \param uuid: UUID of this print job. Should be used for identification purposes. - # \param deleted_at: The time when this print job was deleted. - # \param printed_on_uuid: UUID of the printer used to print this job. - # \param configuration_changes_required: List of configuration changes the printer this job is associated with - # needs to make in order to be able to print this job - # \param build_plate: The build plate (type) this job needs to be printed on. - # \param compatible_machine_families: Family names of machines suitable for this print job - # \param impediments_to_printing: A list of reasons that prevent this job from being printed on the associated - # printer - # \param preview_url: URL to the preview image (same as wou;d've been included in the ufp). def __init__(self, created_at: str, force: bool, machine_variant: str, name: str, started: bool, status: str, time_total: int, uuid: str, configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], @@ -60,6 +33,37 @@ class ClusterPrintJobStatus(BaseModel): impediments_to_printing: List[Union[Dict[str, Any], ClusterPrintJobImpediment]] = None, preview_url: Optional[str] = None, **kwargs) -> None: + + """Creates a new cloud print job status model. + + :param assigned_to: The name of the printer this job is assigned to while being queued. + :param configuration: The required print core configurations of this print job. + :param constraints: Print job constraints object. + :param created_at: The timestamp when the job was created in Cura Connect. + :param force: Allow this job to be printed despite of mismatching configurations. + :param last_seen: The number of seconds since this job was checked. + :param machine_variant: The machine type that this job should be printed on.Coincides with the machine_type field + of the printer object. + :param name: The name of the print job. Usually the name of the .gcode file. + :param network_error_count: The number of errors encountered when requesting data for this print job. + :param owner: The name of the user who added the print job to Cura Connect. + :param printer_uuid: UUID of the printer that the job is currently printing on or assigned to. + :param started: Whether the job has started printing or not. + :param status: The status of the print job. + :param time_elapsed: The remaining printing time in seconds. + :param time_total: The total printing time in seconds. + :param uuid: UUID of this print job. Should be used for identification purposes. + :param deleted_at: The time when this print job was deleted. + :param printed_on_uuid: UUID of the printer used to print this job. + :param configuration_changes_required: List of configuration changes the printer this job is associated with + needs to make in order to be able to print this job + :param build_plate: The build plate (type) this job needs to be printed on. + :param compatible_machine_families: Family names of machines suitable for this print job + :param impediments_to_printing: A list of reasons that prevent this job from being printed on the associated + printer + :param preview_url: URL to the preview image (same as wou;d've been included in the ufp). + """ + self.assigned_to = assigned_to self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) self.constraints = self.parseModels(ClusterPrintJobConstraints, constraints) @@ -90,33 +94,37 @@ class ClusterPrintJobStatus(BaseModel): super().__init__(**kwargs) - ## Creates an UM3 print job output model based on this cloud cluster print job. - # \param printer: The output model of the printer def createOutputModel(self, controller: ClusterOutputController) -> UM3PrintJobOutputModel: + """Creates an UM3 print job output model based on this cloud cluster print job. + + :param printer: The output model of the printer + """ + model = UM3PrintJobOutputModel(controller, self.uuid, self.name) self.updateOutputModel(model) return model - ## Creates a new configuration model def _createConfigurationModel(self) -> PrinterConfigurationModel: + """Creates a new configuration model""" + extruders = [extruder.createConfigurationModel() for extruder in self.configuration or ()] configuration = PrinterConfigurationModel() configuration.setExtruderConfigurations(extruders) configuration.setPrinterType(self.machine_variant) return configuration - ## Updates an UM3 print job output model based on this cloud cluster print job. - # \param model: The model to update. def updateOutputModel(self, model: UM3PrintJobOutputModel) -> None: + """Updates an UM3 print job output model based on this cloud cluster print job. + + :param model: The model to update. + """ + model.updateConfiguration(self._createConfigurationModel()) model.updateTimeTotal(self.time_total) model.updateTimeElapsed(self.time_elapsed) model.updateOwner(self.owner) model.updateState(self.status) model.setCompatibleMachineFamilies(self.compatible_machine_families) - model.updateTimeTotal(self.time_total) - model.updateTimeElapsed(self.time_elapsed) - model.updateOwner(self.owner) status_set_by_impediment = False for impediment in self.impediments_to_printing: diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py index 8edb9fb808..6f2992a03b 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py @@ -9,29 +9,35 @@ from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel from ..BaseModel import BaseModel -## Class representing a cloud cluster printer configuration class ClusterPrinterConfigurationMaterial(BaseModel): + """Class representing a cloud cluster printer configuration""" - ## Creates a new material configuration model. - # \param brand: The brand of material in this print core, e.g. 'Ultimaker'. - # \param color: The color of material in this print core, e.g. 'Blue'. - # \param guid: he GUID of the material in this print core, e.g. '506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9'. - # \param material: The type of material in this print core, e.g. 'PLA'. def __init__(self, brand: Optional[str] = None, color: Optional[str] = None, guid: Optional[str] = None, material: Optional[str] = None, **kwargs) -> None: + + """Creates a new material configuration model. + + :param brand: The brand of material in this print core, e.g. 'Ultimaker'. + :param color: The color of material in this print core, e.g. 'Blue'. + :param guid: he GUID of the material in this print core, e.g. '506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9'. + :param material: The type of material in this print core, e.g. 'PLA'. + """ + self.guid = guid self.brand = brand self.color = color self.material = material super().__init__(**kwargs) - ## Creates a material output model based on this cloud printer material. - # - # A material is chosen that matches the current GUID. If multiple such - # materials are available, read-only materials are preferred and the - # material with the earliest alphabetical name will be selected. - # \return A material output model that matches the current GUID. def createOutputModel(self) -> MaterialOutputModel: + """Creates a material output model based on this cloud printer material. + + A material is chosen that matches the current GUID. If multiple such + materials are available, read-only materials are preferred and the + material with the earliest alphabetical name will be selected. + :return: A material output model that matches the current GUID. + """ + container_registry = ContainerRegistry.getInstance() same_guid = container_registry.findInstanceContainersMetadata(GUID = self.guid) if same_guid: @@ -48,4 +54,4 @@ class ClusterPrinterConfigurationMaterial(BaseModel): "name": "Empty" if self.material == "empty" else "Unknown" } - return MaterialOutputModel(guid = self.guid, type = material_metadata["material"], brand = material_metadata["brand"], color = material_metadata["color_code"], name = material_metadata["name"]) + return MaterialOutputModel(guid = self.guid, type = material_metadata["material"], brand = material_metadata["brand"], color = material_metadata.get("color_code", "#ffc924"), name = material_metadata["name"]) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py index c51e07bcfc..b03a2291c4 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py @@ -6,16 +6,19 @@ from ..BaseModel import BaseModel from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot -## Class representing the data of a Material Station in the cluster. class ClusterPrinterMaterialStation(BaseModel): + """Class representing the data of a Material Station in the cluster.""" - ## Creates a new Material Station status. - # \param status: The status of the material station. - # \param: supported: Whether the material station is supported on this machine or not. - # \param material_slots: The active slots configurations of this material station. def __init__(self, status: str, supported: bool = False, material_slots: List[Union[ClusterPrinterMaterialStationSlot, Dict[str, Any]]] = None, **kwargs) -> None: + """Creates a new Material Station status. + + :param status: The status of the material station. + :param: supported: Whether the material station is supported on this machine or not. + :param material_slots: The active slots configurations of this material station. + """ + self.status = status self.supported = supported self.material_slots = self.parseModels(ClusterPrinterMaterialStationSlot, material_slots)\ diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py index b9c40592e5..11e2736ded 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py @@ -5,16 +5,19 @@ from typing import Optional from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration -## Class representing the data of a single slot in the material station. class ClusterPrinterMaterialStationSlot(ClusterPrintCoreConfiguration): - - ## Create a new material station slot object. - # \param slot_index: The index of the slot in the material station (ranging 0 to 5). - # \param compatible: Whether the configuration is compatible with the print core. - # \param material_remaining: How much material is remaining on the spool (between 0 and 1, or -1 for missing data). - # \param material_empty: Whether the material spool is too empty to be used. + """Class representing the data of a single slot in the material station.""" + def __init__(self, slot_index: int, compatible: bool, material_remaining: float, material_empty: Optional[bool] = False, **kwargs) -> None: + """Create a new material station slot object. + + :param slot_index: The index of the slot in the material station (ranging 0 to 5). + :param compatible: Whether the configuration is compatible with the print core. + :param material_remaining: How much material is remaining on the spool (between 0 and 1, or -1 for missing data). + :param material_empty: Whether the material spool is too empty to be used. + """ + self.slot_index = slot_index self.compatible = compatible self.material_remaining = material_remaining diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 2e0912f057..5b4d7fb161 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -17,26 +17,10 @@ from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMate from ..BaseModel import BaseModel -## Class representing a cluster printer class ClusterPrinterStatus(BaseModel): + """Class representing a cluster printer""" + - ## Creates a new cluster printer status - # \param enabled: A printer can be disabled if it should not receive new jobs. By default every printer is enabled. - # \param firmware_version: Firmware version installed on the printer. Can differ for each printer in a cluster. - # \param friendly_name: Human readable name of the printer. Can be used for identification purposes. - # \param ip_address: The IP address of the printer in the local network. - # \param machine_variant: The type of printer. Can be 'Ultimaker 3' or 'Ultimaker 3ext'. - # \param status: The status of the printer. - # \param unique_name: The unique name of the printer in the network. - # \param uuid: The unique ID of the printer, also known as GUID. - # \param configuration: The active print core configurations of this printer. - # \param reserved_by: A printer can be claimed by a specific print job. - # \param maintenance_required: Indicates if maintenance is necessary. - # \param firmware_update_status: Whether the printer's firmware is up-to-date, value is one of: "up_to_date", - # "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible". - # \param latest_available_firmware: The version of the latest firmware that is available. - # \param build_plate: The build plate that is on the printer. - # \param material_station: The material station that is on the printer. def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str, status: str, unique_name: str, uuid: str, configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], @@ -44,6 +28,25 @@ class ClusterPrinterStatus(BaseModel): firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None, build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None: + """Creates a new cluster printer status + + :param enabled: A printer can be disabled if it should not receive new jobs. By default every printer is enabled. + :param firmware_version: Firmware version installed on the printer. Can differ for each printer in a cluster. + :param friendly_name: Human readable name of the printer. Can be used for identification purposes. + :param ip_address: The IP address of the printer in the local network. + :param machine_variant: The type of printer. Can be 'Ultimaker 3' or 'Ultimaker 3ext'. + :param status: The status of the printer. + :param unique_name: The unique name of the printer in the network. + :param uuid: The unique ID of the printer, also known as GUID. + :param configuration: The active print core configurations of this printer. + :param reserved_by: A printer can be claimed by a specific print job. + :param maintenance_required: Indicates if maintenance is necessary. + :param firmware_update_status: Whether the printer's firmware is up-to-date, value is one of: "up_to_date", + "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible". + :param latest_available_firmware: The version of the latest firmware that is available. + :param build_plate: The build plate that is on the printer. + :param material_station: The material station that is on the printer. + """ self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) self.enabled = enabled @@ -63,9 +66,12 @@ class ClusterPrinterStatus(BaseModel): material_station) if material_station else None super().__init__(**kwargs) - ## Creates a new output model. - # \param controller - The controller of the model. def createOutputModel(self, controller: PrinterOutputController) -> PrinterOutputModel: + """Creates a new output model. + + :param controller: - The controller of the model. + """ + # FIXME # Note that we're using '2' here as extruder count. We have hardcoded this for now to prevent issues where the # amount of extruders coming back from the API is actually lower (which it can be if a printer was just added @@ -74,9 +80,12 @@ class ClusterPrinterStatus(BaseModel): self.updateOutputModel(model) return model - ## Updates the given output model. - # \param model - The output model to update. def updateOutputModel(self, model: PrinterOutputModel) -> None: + """Updates the given output model. + + :param model: - The output model to update. + """ + model.updateKey(self.uuid) model.updateName(self.friendly_name) model.updateUniqueName(self.unique_name) @@ -110,9 +119,12 @@ class ClusterPrinterStatus(BaseModel): ) for left_slot, right_slot in product(self._getSlotsForExtruder(0), self._getSlotsForExtruder(1))] model.setAvailableConfigurations(available_configurations) - ## Create a list of Material Station slots for the given extruder index. - # Returns a list with a single empty material slot if none are found to ensure we don't miss configurations. def _getSlotsForExtruder(self, extruder_index: int) -> List[ClusterPrinterMaterialStationSlot]: + """Create a list of Material Station slots for the given extruder index. + + Returns a list with a single empty material slot if none are found to ensure we don't miss configurations. + """ + if not self.material_station: # typing guard return [] slots = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration( @@ -121,15 +133,19 @@ class ClusterPrinterStatus(BaseModel): )] return slots or [self._createEmptyMaterialSlot(extruder_index)] - ## Check if a configuration is supported in order to make it selectable by the user. - # We filter out any slot that is not supported by the extruder index, print core type or if the material is empty. @staticmethod def _isSupportedConfiguration(slot: ClusterPrinterMaterialStationSlot, extruder_index: int) -> bool: + """Check if a configuration is supported in order to make it selectable by the user. + + We filter out any slot that is not supported by the extruder index, print core type or if the material is empty. + """ + return slot.extruder_index == extruder_index and slot.compatible and not slot.material_empty - ## Create an empty material slot with a fake empty material. @staticmethod def _createEmptyMaterialSlot(extruder_index: int) -> ClusterPrinterMaterialStationSlot: + """Create an empty material slot with a fake empty material.""" + empty_material = ClusterPrinterConfigurationMaterial(guid = "", material = "empty", brand = "", color = "") return ClusterPrinterMaterialStationSlot(slot_index = 0, extruder_index = extruder_index, compatible = True, material_remaining = 0, material = empty_material) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py index ad7b9c8698..58d6c94ee1 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py @@ -5,12 +5,11 @@ from typing import Dict, Any from ..BaseModel import BaseModel -## Class representing the system status of a printer. class PrinterSystemStatus(BaseModel): + """Class representing the system status of a printer.""" def __init__(self, guid: str, firmware: str, hostname: str, name: str, platform: str, variant: str, - hardware: Dict[str, Any], **kwargs - ) -> None: + hardware: Dict[str, Any], **kwargs) -> None: self.guid = guid self.firmware = firmware self.hostname = hostname diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index 6a8b9f625c..d1840bf90c 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -16,12 +16,13 @@ from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus from ..Models.Http.ClusterMaterial import ClusterMaterial -## The generic type variable used to document the methods below. ClusterApiClientModel = TypeVar("ClusterApiClientModel", bound=BaseModel) +"""The generic type variable used to document the methods below.""" -## The ClusterApiClient is responsible for all network calls to local network clusters. class ClusterApiClient: + """The ClusterApiClient is responsible for all network calls to local network clusters.""" + PRINTER_API_PREFIX = "/api/v1" CLUSTER_API_PREFIX = "/cluster-api/v1" @@ -29,75 +30,92 @@ class ClusterApiClient: # In order to avoid garbage collection we keep the callbacks in this list. _anti_gc_callbacks = [] # type: List[Callable[[], None]] - ## Initializes a new cluster API client. - # \param address: The network address of the cluster to call. - # \param on_error: The callback to be called whenever we receive errors from the server. def __init__(self, address: str, on_error: Callable) -> None: + """Initializes a new cluster API client. + + :param address: The network address of the cluster to call. + :param on_error: The callback to be called whenever we receive errors from the server. + """ super().__init__() self._manager = QNetworkAccessManager() self._address = address self._on_error = on_error - ## Get printer system information. - # \param on_finished: The callback in case the response is successful. def getSystem(self, on_finished: Callable) -> None: + """Get printer system information. + + :param on_finished: The callback in case the response is successful. + """ url = "{}/system".format(self.PRINTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, PrinterSystemStatus) - ## Get the installed materials on the printer. - # \param on_finished: The callback in case the response is successful. def getMaterials(self, on_finished: Callable[[List[ClusterMaterial]], Any]) -> None: + """Get the installed materials on the printer. + + :param on_finished: The callback in case the response is successful. + """ url = "{}/materials".format(self.CLUSTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterMaterial) - ## Get the printers in the cluster. - # \param on_finished: The callback in case the response is successful. def getPrinters(self, on_finished: Callable[[List[ClusterPrinterStatus]], Any]) -> None: + """Get the printers in the cluster. + + :param on_finished: The callback in case the response is successful. + """ url = "{}/printers".format(self.CLUSTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterPrinterStatus) - ## Get the print jobs in the cluster. - # \param on_finished: The callback in case the response is successful. def getPrintJobs(self, on_finished: Callable[[List[ClusterPrintJobStatus]], Any]) -> None: + """Get the print jobs in the cluster. + + :param on_finished: The callback in case the response is successful. + """ url = "{}/print_jobs".format(self.CLUSTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterPrintJobStatus) - ## Move a print job to the top of the queue. def movePrintJobToTop(self, print_job_uuid: str) -> None: + """Move a print job to the top of the queue.""" + url = "{}/print_jobs/{}/action/move".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.post(self._createEmptyRequest(url), json.dumps({"to_position": 0, "list": "queued"}).encode()) - ## Override print job configuration and force it to be printed. def forcePrintJob(self, print_job_uuid: str) -> None: + """Override print job configuration and force it to be printed.""" + url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.put(self._createEmptyRequest(url), json.dumps({"force": True}).encode()) - ## Delete a print job from the queue. def deletePrintJob(self, print_job_uuid: str) -> None: + """Delete a print job from the queue.""" + url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.deleteResource(self._createEmptyRequest(url)) - ## Set the state of a print job. def setPrintJobState(self, print_job_uuid: str, state: str) -> None: + """Set the state of a print job.""" + url = "{}/print_jobs/{}/action".format(self.CLUSTER_API_PREFIX, print_job_uuid) # We rewrite 'resume' to 'print' here because we are using the old print job action endpoints. action = "print" if state == "resume" else state self._manager.put(self._createEmptyRequest(url), json.dumps({"action": action}).encode()) - ## Get the preview image data of a print job. def getPrintJobPreviewImage(self, print_job_uuid: str, on_finished: Callable) -> None: + """Get the preview image data of a print job.""" + url = "{}/print_jobs/{}/preview_image".format(self.CLUSTER_API_PREFIX, print_job_uuid) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished) - ## We override _createEmptyRequest in order to add the user credentials. - # \param url: The URL to request - # \param content_type: The type of the body contents. def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest: + """We override _createEmptyRequest in order to add the user credentials. + + :param url: The URL to request + :param content_type: The type of the body contents. + """ url = QUrl("http://" + self._address + path) request = QNetworkRequest(url) request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) @@ -105,11 +123,13 @@ class ClusterApiClient: request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) return request - ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. - # \param reply: The reply from the server. - # \return A tuple with a status code and a dictionary. @staticmethod def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: + """Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. + + :param reply: The reply from the server. + :return: A tuple with a status code and a dictionary. + """ status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) try: response = bytes(reply.readAll()).decode() @@ -118,14 +138,15 @@ class ClusterApiClient: Logger.logException("e", "Could not parse the cluster response: %s", err) return status_code, {"errors": [err]} - ## Parses the given models and calls the correct callback depending on the result. - # \param response: The response from the server, after being converted to a dict. - # \param on_finished: The callback in case the response is successful. - # \param model_class: The type of the model to convert the response to. It may either be a single record or a list. - def _parseModels(self, response: Dict[str, Any], - on_finished: Union[Callable[[ClusterApiClientModel], Any], - Callable[[List[ClusterApiClientModel]], Any]], - model_class: Type[ClusterApiClientModel]) -> None: + def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[ClusterApiClientModel], Any], + Callable[[List[ClusterApiClientModel]], Any]], model_class: Type[ClusterApiClientModel]) -> None: + """Parses the given models and calls the correct callback depending on the result. + + :param response: The response from the server, after being converted to a dict. + :param on_finished: The callback in case the response is successful. + :param model_class: The type of the model to convert the response to. It may either be a single record or a list. + """ + try: if isinstance(response, list): results = [model_class(**c) for c in response] # type: List[ClusterApiClientModel] @@ -138,16 +159,15 @@ class ClusterApiClient: except (JSONDecodeError, TypeError, ValueError): Logger.log("e", "Could not parse response from network: %s", str(response)) - ## Creates a callback function so that it includes the parsing of the response into the correct model. - # The callback is added to the 'finished' signal of the reply. - # \param reply: The reply that should be listened to. - # \param on_finished: The callback in case the response is successful. - def _addCallback(self, - reply: QNetworkReply, - on_finished: Union[Callable[[ClusterApiClientModel], Any], - Callable[[List[ClusterApiClientModel]], Any]], - model: Type[ClusterApiClientModel] = None, + def _addCallback(self, reply: QNetworkReply, on_finished: Union[Callable[[ClusterApiClientModel], Any], + Callable[[List[ClusterApiClientModel]], Any]], model: Type[ClusterApiClientModel] = None, ) -> None: + """Creates a callback function so that it includes the parsing of the response into the correct model. + + The callback is added to the 'finished' signal of the reply. + :param reply: The reply that should be listened to. + :param on_finished: The callback in case the response is successful. + """ def parse() -> None: self._anti_gc_callbacks.remove(parse) diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py index 1266afcca8..48e552241a 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py @@ -51,15 +51,17 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._setInterfaceElements() self._active_camera_url = QUrl() # type: QUrl - ## Set all the interface elements and texts for this output device. def _setInterfaceElements(self) -> None: + """Set all the interface elements and texts for this output device.""" + self.setPriority(3) # Make sure the output device gets selected above local file output self.setShortDescription(I18N_CATALOG.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print over network")) self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected over the network")) - ## Called when the connection to the cluster changes. def connect(self) -> None: + """Called when the connection to the cluster changes.""" + super().connect() self._update() self.sendMaterialProfiles() @@ -94,10 +96,13 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): def forceSendJob(self, print_job_uuid: str) -> None: self._getApiClient().forcePrintJob(print_job_uuid) - ## Set the remote print job state. - # \param print_job_uuid: The UUID of the print job to set the state for. - # \param action: The action to undertake ('pause', 'resume', 'abort'). def setJobState(self, print_job_uuid: str, action: str) -> None: + """Set the remote print job state. + + :param print_job_uuid: The UUID of the print job to set the state for. + :param action: The action to undertake ('pause', 'resume', 'abort'). + """ + self._getApiClient().setPrintJobState(print_job_uuid, action) def _update(self) -> None: @@ -106,19 +111,22 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._getApiClient().getPrintJobs(self._updatePrintJobs) self._updatePrintJobPreviewImages() - ## Get a list of materials that are installed on the cluster host. def getMaterials(self, on_finished: Callable[[List[ClusterMaterial]], Any]) -> None: + """Get a list of materials that are installed on the cluster host.""" + self._getApiClient().getMaterials(on_finished = on_finished) - ## Sync the material profiles in Cura with the printer. - # This gets called when connecting to a printer as well as when sending a print. def sendMaterialProfiles(self) -> None: + """Sync the material profiles in Cura with the printer. + + This gets called when connecting to a printer as well as when sending a print. + """ job = SendMaterialJob(device = self) job.run() - ## Send a print job to the cluster. def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None: + """Send a print job to the cluster.""" # Show an error message if we're already sending a job. if self._progress.visible: @@ -132,15 +140,20 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): job.finished.connect(self._onPrintJobCreated) job.start() - ## Allows the user to choose a printer to print with from the printer selection dialogue. - # \param unique_name: The unique name of the printer to target. @pyqtSlot(str, name="selectTargetPrinter") def selectTargetPrinter(self, unique_name: str = "") -> None: + """Allows the user to choose a printer to print with from the printer selection dialogue. + + :param unique_name: The unique name of the printer to target. + """ self._startPrintJobUpload(unique_name if unique_name != "" else None) - ## Handler for when the print job was created locally. - # It can now be sent over the network. def _onPrintJobCreated(self, job: ExportFileJob) -> None: + """Handler for when the print job was created locally. + + It can now be sent over the network. + """ + self._active_exported_job = job # TODO: add preference to enable/disable this feature? if self.clusterSize > 1: @@ -148,8 +161,9 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): return self._startPrintJobUpload() - ## Shows a dialog allowing the user to select which printer in a group to send a job to. def _showPrinterSelectionDialog(self) -> None: + """Shows a dialog allowing the user to select which printer in a group to send a job to.""" + if not self._printer_select_dialog: plugin_path = CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or "" path = os.path.join(plugin_path, "resources", "qml", "PrintWindow.qml") @@ -157,8 +171,9 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): if self._printer_select_dialog is not None: self._printer_select_dialog.show() - ## Upload the print job to the group. def _startPrintJobUpload(self, unique_name: str = None) -> None: + """Upload the print job to the group.""" + if not self._active_exported_job: Logger.log("e", "No active exported job to upload!") return @@ -177,33 +192,40 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): on_progress=self._onPrintJobUploadProgress) self._active_exported_job = None - ## Handler for print job upload progress. def _onPrintJobUploadProgress(self, bytes_sent: int, bytes_total: int) -> None: + """Handler for print job upload progress.""" + percentage = (bytes_sent / bytes_total) if bytes_total else 0 self._progress.setProgress(percentage * 100) self.writeProgress.emit() - ## Handler for when the print job was fully uploaded to the cluster. def _onPrintUploadCompleted(self, _: QNetworkReply) -> None: + """Handler for when the print job was fully uploaded to the cluster.""" + self._progress.hide() PrintJobUploadSuccessMessage().show() self.writeFinished.emit() - ## Displays the given message if uploading the mesh has failed - # \param message: The message to display. def _onUploadError(self, message: str = None) -> None: + """Displays the given message if uploading the mesh has failed + + :param message: The message to display. + """ + self._progress.hide() PrintJobUploadErrorMessage(message).show() self.writeError.emit() - ## Download all the images from the cluster and load their data in the print job models. def _updatePrintJobPreviewImages(self): + """Download all the images from the cluster and load their data in the print job models.""" + for print_job in self._print_jobs: if print_job.getPreviewImage() is None: self._getApiClient().getPrintJobPreviewImage(print_job.key, print_job.updatePreviewImageData) - ## Get the API client instance. def _getApiClient(self) -> ClusterApiClient: + """Get the API client instance.""" + if not self._cluster_api: self._cluster_api = ClusterApiClient(self.address, on_error = lambda error: Logger.log("e", str(error))) return self._cluster_api diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py index 273c64ef4d..e79709d3dc 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py @@ -24,8 +24,9 @@ from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus I18N_CATALOG = i18nCatalog("cura") -## The LocalClusterOutputDeviceManager is responsible for discovering and managing local networked clusters. class LocalClusterOutputDeviceManager: + """The LocalClusterOutputDeviceManager is responsible for discovering and managing local networked clusters.""" + META_NETWORK_KEY = "um_network_key" @@ -49,30 +50,35 @@ class LocalClusterOutputDeviceManager: self._zero_conf_client.addedNetworkCluster.connect(self._onDeviceDiscovered) self._zero_conf_client.removedNetworkCluster.connect(self._onDiscoveredDeviceRemoved) - ## Start the network discovery. def start(self) -> None: + """Start the network discovery.""" + self._zero_conf_client.start() for address in self._getStoredManualAddresses(): self.addManualDevice(address) - ## Stop network discovery and clean up discovered devices. def stop(self) -> None: + """Stop network discovery and clean up discovered devices.""" + self._zero_conf_client.stop() for instance_name in list(self._discovered_devices): self._onDiscoveredDeviceRemoved(instance_name) - ## Restart discovery on the local network. def startDiscovery(self): + """Restart discovery on the local network.""" + self.stop() self.start() - ## Add a networked printer manually by address. def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: + """Add a networked printer manually by address.""" + api_client = ClusterApiClient(address, lambda error: Logger.log("e", str(error))) api_client.getSystem(lambda status: self._onCheckManualDeviceResponse(address, status, callback)) - ## Remove a manually added networked printer. def removeManualDevice(self, device_id: str, address: Optional[str] = None) -> None: + """Remove a manually added networked printer.""" + if device_id not in self._discovered_devices and address is not None: device_id = "manual:{}".format(address) @@ -83,16 +89,19 @@ class LocalClusterOutputDeviceManager: if address in self._getStoredManualAddresses(): self._removeStoredManualAddress(address) - ## Force reset all network device connections. def refreshConnections(self) -> None: + """Force reset all network device connections.""" + self._connectToActiveMachine() - ## Get the discovered devices. def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]: + """Get the discovered devices.""" + return self._discovered_devices - ## Connect the active machine to a given device. def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: + """Connect the active machine to a given device.""" + active_machine = CuraApplication.getInstance().getGlobalContainerStack() if not active_machine: return @@ -106,8 +115,9 @@ class LocalClusterOutputDeviceManager: return CuraApplication.getInstance().getMachineManager().switchPrinterType(definitions[0].getName()) - ## Callback for when the active machine was changed by the user or a new remote cluster was found. def _connectToActiveMachine(self) -> None: + """Callback for when the active machine was changed by the user or a new remote cluster was found.""" + active_machine = CuraApplication.getInstance().getGlobalContainerStack() if not active_machine: return @@ -122,9 +132,10 @@ class LocalClusterOutputDeviceManager: # Remove device if it is not meant for the active machine. output_device_manager.removeOutputDevice(device.key) - ## Callback for when a manual device check request was responded to. def _onCheckManualDeviceResponse(self, address: str, status: PrinterSystemStatus, callback: Optional[Callable[[bool, str], None]] = None) -> None: + """Callback for when a manual device check request was responded to.""" + self._onDeviceDiscovered("manual:{}".format(address), address, { b"name": status.name.encode("utf-8"), b"address": address.encode("utf-8"), @@ -137,10 +148,13 @@ class LocalClusterOutputDeviceManager: if callback is not None: CuraApplication.getInstance().callLater(callback, True, address) - ## Returns a dict of printer BOM numbers to machine types. - # These numbers are available in the machine definition already so we just search for them here. @staticmethod def _getPrinterTypeIdentifiers() -> Dict[str, str]: + """Returns a dict of printer BOM numbers to machine types. + + These numbers are available in the machine definition already so we just search for them here. + """ + container_registry = CuraApplication.getInstance().getContainerRegistry() ultimaker_machines = container_registry.findContainersMetadata(type="machine", manufacturer="Ultimaker B.V.") found_machine_type_identifiers = {} # type: Dict[str, str] @@ -154,8 +168,9 @@ class LocalClusterOutputDeviceManager: found_machine_type_identifiers[str(bom_number)] = machine_type return found_machine_type_identifiers - ## Add a new device. def _onDeviceDiscovered(self, key: str, address: str, properties: Dict[bytes, bytes]) -> None: + """Add a new device.""" + machine_identifier = properties.get(b"machine", b"").decode("utf-8") printer_type_identifiers = self._getPrinterTypeIdentifiers() @@ -189,8 +204,9 @@ class LocalClusterOutputDeviceManager: self.discoveredDevicesChanged.emit() self._connectToActiveMachine() - ## Remove a device. def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: + """Remove a device.""" + device = self._discovered_devices.pop(device_id, None) # type: Optional[LocalClusterOutputDevice] if not device: return @@ -198,8 +214,9 @@ class LocalClusterOutputDeviceManager: CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address) self.discoveredDevicesChanged.emit() - ## Create a machine instance based on the discovered network printer. def _createMachineFromDiscoveredDevice(self, device_id: str) -> None: + """Create a machine instance based on the discovered network printer.""" + device = self._discovered_devices.get(device_id) if device is None: return @@ -216,8 +233,9 @@ class LocalClusterOutputDeviceManager: self._connectToOutputDevice(device, new_machine) self._showCloudFlowMessage(device) - ## Add an address to the stored preferences. def _storeManualAddress(self, address: str) -> None: + """Add an address to the stored preferences.""" + stored_addresses = self._getStoredManualAddresses() if address in stored_addresses: return # Prevent duplicates. @@ -225,8 +243,9 @@ class LocalClusterOutputDeviceManager: new_value = ",".join(stored_addresses) CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_value) - ## Remove an address from the stored preferences. def _removeStoredManualAddress(self, address: str) -> None: + """Remove an address from the stored preferences.""" + stored_addresses = self._getStoredManualAddresses() try: stored_addresses.remove(address) # Can throw a ValueError @@ -235,15 +254,16 @@ class LocalClusterOutputDeviceManager: except ValueError: Logger.log("w", "Could not remove address from stored_addresses, it was not there") - ## Load the user-configured manual devices from Cura preferences. def _getStoredManualAddresses(self) -> List[str]: + """Load the user-configured manual devices from Cura preferences.""" + preferences = CuraApplication.getInstance().getPreferences() preferences.addPreference(self.MANUAL_DEVICES_PREFERENCE_KEY, "") manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",") return manual_instances - ## Add a device to the current active machine. def _connectToOutputDevice(self, device: UltimakerNetworkedPrinterOutputDevice, machine: GlobalStack) -> None: + """Add a device to the current active machine.""" # Make sure users know that we no longer support legacy devices. if Version(device.firmwareVersion) < self.MIN_SUPPORTED_CLUSTER_VERSION: @@ -262,10 +282,11 @@ class LocalClusterOutputDeviceManager: if device.key not in output_device_manager.getOutputDeviceIds(): output_device_manager.addOutputDevice(device) - ## Nudge the user to start using Ultimaker Cloud. @staticmethod def _showCloudFlowMessage(device: LocalClusterOutputDevice) -> None: - if CuraApplication.getInstance().getMachineManager().activeMachineIsUsingCloudConnection: + """Nudge the user to start using Ultimaker Cloud.""" + + if CuraApplication.getInstance().getMachineManager().activeMachineHasCloudRegistration: # This printer is already cloud connected, so we do not bother the user anymore. return if not CuraApplication.getInstance().getCuraAPI().account.isLoggedIn: diff --git a/plugins/UM3NetworkPrinting/src/Network/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/Network/SendMaterialJob.py index 49e088100d..2740f86605 100644 --- a/plugins/UM3NetworkPrinting/src/Network/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/Network/SendMaterialJob.py @@ -16,27 +16,33 @@ if TYPE_CHECKING: from .LocalClusterOutputDevice import LocalClusterOutputDevice -## Asynchronous job to send material profiles to the printer. -# -# This way it won't freeze up the interface while sending those materials. class SendMaterialJob(Job): + """Asynchronous job to send material profiles to the printer. + + This way it won't freeze up the interface while sending those materials. + """ + def __init__(self, device: "LocalClusterOutputDevice") -> None: super().__init__() self.device = device # type: LocalClusterOutputDevice - ## Send the request to the printer and register a callback def run(self) -> None: + """Send the request to the printer and register a callback""" + self.device.getMaterials(on_finished = self._onGetMaterials) - ## Callback for when the remote materials were returned. def _onGetMaterials(self, materials: List[ClusterMaterial]) -> None: + """Callback for when the remote materials were returned.""" + remote_materials_by_guid = {material.guid: material for material in materials} self._sendMissingMaterials(remote_materials_by_guid) - ## Determine which materials should be updated and send them to the printer. - # \param remote_materials_by_guid The remote materials by GUID. def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None: + """Determine which materials should be updated and send them to the printer. + + :param remote_materials_by_guid: The remote materials by GUID. + """ local_materials_by_guid = self._getLocalMaterials() if len(local_materials_by_guid) == 0: Logger.log("d", "There are no local materials to synchronize with the printer.") @@ -47,25 +53,31 @@ class SendMaterialJob(Job): return self._sendMaterials(material_ids_to_send) - ## From the local and remote materials, determine which ones should be synchronized. - # Makes a Set of id's containing only the id's of the materials that are not on the printer yet or the ones that - # are newer in Cura. - # \param local_materials The local materials by GUID. - # \param remote_materials The remote materials by GUID. @staticmethod def _determineMaterialsToSend(local_materials: Dict[str, LocalMaterial], remote_materials: Dict[str, ClusterMaterial]) -> Set[str]: + """From the local and remote materials, determine which ones should be synchronized. + + Makes a Set of id's containing only the id's of the materials that are not on the printer yet or the ones that + are newer in Cura. + :param local_materials: The local materials by GUID. + :param remote_materials: The remote materials by GUID. + """ + return { local_material.id for guid, local_material in local_materials.items() if guid not in remote_materials.keys() or local_material.version > remote_materials[guid].version } - ## Send the materials to the printer. - # The given materials will be loaded from disk en sent to to printer. - # The given id's will be matched with filenames of the locally stored materials. - # \param materials_to_send A set with id's of materials that must be sent. def _sendMaterials(self, materials_to_send: Set[str]) -> None: + """Send the materials to the printer. + + The given materials will be loaded from disk en sent to to printer. + The given id's will be matched with filenames of the locally stored materials. + :param materials_to_send: A set with id's of materials that must be sent. + """ + container_registry = CuraApplication.getInstance().getContainerRegistry() all_materials = container_registry.findInstanceContainersMetadata(type = "material") all_base_files = {material["base_file"] for material in all_materials if "base_file" in material} # Filters out uniques by making it a set. Don't include files without base file (i.e. empty material). @@ -83,12 +95,14 @@ class SendMaterialJob(Job): file_name = os.path.basename(file_path) self._sendMaterialFile(file_path, file_name, root_material_id) - ## Send a single material file to the printer. - # Also add the material signature file if that is available. - # \param file_path The path of the material file. - # \param file_name The name of the material file. - # \param material_id The ID of the material in the file. def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None: + """Send a single material file to the printer. + + Also add the material signature file if that is available. + :param file_path: The path of the material file. + :param file_name: The name of the material file. + :param material_id: The ID of the material in the file. + """ parts = [] # Add the material file. @@ -112,8 +126,9 @@ class SendMaterialJob(Job): self.device.postFormWithParts(target = "/cluster-api/v1/materials/", parts = parts, on_finished = self._sendingFinished) - ## Check a reply from an upload to the printer and log an error when the call failed def _sendingFinished(self, reply: QNetworkReply) -> None: + """Check a reply from an upload to the printer and log an error when the call failed""" + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: Logger.log("w", "Error while syncing material: %s", reply.errorString()) return @@ -125,11 +140,14 @@ class SendMaterialJob(Job): # Because of the guards above it is not shown when syncing failed (which is not always an actual problem). MaterialSyncMessage(self.device).show() - ## Retrieves a list of local materials - # Only the new newest version of the local materials is returned - # \return a dictionary of LocalMaterial objects by GUID @staticmethod def _getLocalMaterials() -> Dict[str, LocalMaterial]: + """Retrieves a list of local materials + + Only the new newest version of the local materials is returned + :return: a dictionary of LocalMaterial objects by GUID + """ + result = {} # type: Dict[str, LocalMaterial] all_materials = CuraApplication.getInstance().getContainerRegistry().findInstanceContainersMetadata(type = "material") all_base_files = [material for material in all_materials if material["id"] == material.get("base_file")] # Don't send materials without base_file: The empty material doesn't need to be sent. diff --git a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py index 466638d99e..d59f2f2893 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py @@ -12,9 +12,11 @@ from UM.Signal import Signal from cura.CuraApplication import CuraApplication -## The ZeroConfClient handles all network discovery logic. -# It emits signals when new network services were found or disappeared. class ZeroConfClient: + """The ZeroConfClient handles all network discovery logic. + + It emits signals when new network services were found or disappeared. + """ # The discovery protocol name for Ultimaker printers. ZERO_CONF_NAME = u"_ultimaker._tcp.local." @@ -30,10 +32,13 @@ class ZeroConfClient: self._service_changed_request_event = None # type: Optional[Event] self._service_changed_request_thread = None # type: Optional[Thread] - ## The ZeroConf service changed requests are handled in a separate thread so we don't block the UI. - # We can also re-schedule the requests when they fail to get detailed service info. - # Any new or re-reschedule requests will be appended to the request queue and the thread will process them. def start(self) -> None: + """The ZeroConf service changed requests are handled in a separate thread so we don't block the UI. + + We can also re-schedule the requests when they fail to get detailed service info. + Any new or re-reschedule requests will be appended to the request queue and the thread will process them. + """ + self._service_changed_request_queue = Queue() self._service_changed_request_event = Event() try: @@ -56,16 +61,18 @@ class ZeroConfClient: self._zero_conf_browser.cancel() self._zero_conf_browser = None - ## Handles a change is discovered network services. def _queueService(self, zeroconf: Zeroconf, service_type, name: str, state_change: ServiceStateChange) -> None: + """Handles a change is discovered network services.""" + item = (zeroconf, service_type, name, state_change) if not self._service_changed_request_queue or not self._service_changed_request_event: return self._service_changed_request_queue.put(item) self._service_changed_request_event.set() - ## Callback for when a ZeroConf service has changes. def _handleOnServiceChangedRequests(self) -> None: + """Callback for when a ZeroConf service has changes.""" + if not self._service_changed_request_queue or not self._service_changed_request_event: return @@ -98,19 +105,23 @@ class ZeroConfClient: for request in reschedule_requests: self._service_changed_request_queue.put(request) - ## Handler for zeroConf detection. - # Return True or False indicating if the process succeeded. - # Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread. - def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange - ) -> bool: + def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, + state_change: ServiceStateChange) -> bool: + """Handler for zeroConf detection. + + Return True or False indicating if the process succeeded. + Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread. + """ + if state_change == ServiceStateChange.Added: return self._onServiceAdded(zero_conf, service_type, name) elif state_change == ServiceStateChange.Removed: return self._onServiceRemoved(name) return True - ## Handler for when a ZeroConf service was added. def _onServiceAdded(self, zero_conf: Zeroconf, service_type: str, name: str) -> bool: + """Handler for when a ZeroConf service was added.""" + # First try getting info from zero-conf cache info = ServiceInfo(service_type, name, properties={}) for record in zero_conf.cache.entries_with_name(name.lower()): @@ -141,8 +152,9 @@ class ZeroConfClient: return True - ## Handler for when a ZeroConf service was removed. def _onServiceRemoved(self, name: str) -> bool: + """Handler for when a ZeroConf service was removed.""" + Logger.log("d", "ZeroConf service removed: %s" % name) self.removedNetworkCluster.emit(str(name)) return True diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 3ab37297b5..44dbfbb5f5 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -13,11 +13,11 @@ from .Network.LocalClusterOutputDeviceManager import LocalClusterOutputDeviceMan from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager -## This plugin handles the discovery and networking for Ultimaker 3D printers that support network and cloud printing. class UM3OutputDevicePlugin(OutputDevicePlugin): - - # Signal emitted when the list of discovered devices changed. Used by printer action in this plugin. + """This plugin handles the discovery and networking for Ultimaker 3D printers""" + discoveredDevicesChanged = Signal() + """Signal emitted when the list of discovered devices changed. Used by printer action in this plugin.""" def __init__(self) -> None: super().__init__() @@ -33,8 +33,9 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): # This ensures no output devices are still connected that do not belong to the new active machine. CuraApplication.getInstance().globalContainerStackChanged.connect(self.refreshConnections) - ## Start looking for devices in the network and cloud. def start(self): + """Start looking for devices in the network and cloud.""" + self._network_output_device_manager.start() self._cloud_output_device_manager.start() @@ -43,31 +44,38 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._network_output_device_manager.stop() self._cloud_output_device_manager.stop() - ## Restart network discovery. def startDiscovery(self) -> None: + """Restart network discovery.""" + self._network_output_device_manager.startDiscovery() - ## Force refreshing the network connections. def refreshConnections(self) -> None: + """Force refreshing the network connections.""" + self._network_output_device_manager.refreshConnections() self._cloud_output_device_manager.refreshConnections() - ## Indicate that this plugin supports adding networked printers manually. def canAddManualDevice(self, address: str = "") -> ManualDeviceAdditionAttempt: + """Indicate that this plugin supports adding networked printers manually.""" + return ManualDeviceAdditionAttempt.PRIORITY - ## Add a networked printer manually based on its network address. def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: + """Add a networked printer manually based on its network address.""" + self._network_output_device_manager.addManualDevice(address, callback) - ## Remove a manually connected networked printer. def removeManualDevice(self, key: str, address: Optional[str] = None) -> None: + """Remove a manually connected networked printer.""" + self._network_output_device_manager.removeManualDevice(key, address) - - ## Get the discovered devices from the local network. + def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]: + """Get the discovered devices from the local network.""" + return self._network_output_device_manager.getDiscoveredDevices() - ## Connect the active machine to a device. def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: + """Connect the active machine to a device.""" + self._network_output_device_manager.associateActiveMachineWithPrinterDevice(device) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py index 8c5f5c12ea..772a9d1973 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py @@ -15,9 +15,11 @@ from .Network.LocalClusterOutputDevice import LocalClusterOutputDevice I18N_CATALOG = i18nCatalog("cura") -## Machine action that allows to connect the active machine to a networked devices. -# TODO: in the future this should be part of the new discovery workflow baked into Cura. class UltimakerNetworkedPrinterAction(MachineAction): + """Machine action that allows to connect the active machine to a networked devices. + + TODO: in the future this should be part of the new discovery workflow baked into Cura. + """ # Signal emitted when discovered devices have changed. discoveredDevicesChanged = pyqtSignal() @@ -27,59 +29,69 @@ class UltimakerNetworkedPrinterAction(MachineAction): self._qml_url = "resources/qml/DiscoverUM3Action.qml" self._network_plugin = None # type: Optional[UM3OutputDevicePlugin] - ## Override the default value. def needsUserInteraction(self) -> bool: + """Override the default value.""" + return False - ## Start listening to network discovery events via the plugin. @pyqtSlot(name = "startDiscovery") def startDiscovery(self) -> None: + """Start listening to network discovery events via the plugin.""" + self._networkPlugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged) self.discoveredDevicesChanged.emit() # trigger at least once to populate the list - ## Reset the discovered devices. @pyqtSlot(name = "reset") def reset(self) -> None: + """Reset the discovered devices.""" + self.discoveredDevicesChanged.emit() # trigger to reset the list - ## Reset the discovered devices. @pyqtSlot(name = "restartDiscovery") def restartDiscovery(self) -> None: + """Reset the discovered devices.""" + self._networkPlugin.startDiscovery() self.discoveredDevicesChanged.emit() # trigger to reset the list - ## Remove a manually added device. @pyqtSlot(str, str, name = "removeManualDevice") def removeManualDevice(self, key: str, address: str) -> None: + """Remove a manually added device.""" + self._networkPlugin.removeManualDevice(key, address) - ## Add a new manual device. Can replace an existing one by key. @pyqtSlot(str, str, name = "setManualDevice") def setManualDevice(self, key: str, address: str) -> None: + """Add a new manual device. Can replace an existing one by key.""" + if key != "": self._networkPlugin.removeManualDevice(key) if address != "": self._networkPlugin.addManualDevice(address) - ## Get the devices discovered in the local network sorted by name. @pyqtProperty("QVariantList", notify = discoveredDevicesChanged) def foundDevices(self): + """Get the devices discovered in the local network sorted by name.""" + discovered_devices = list(self._networkPlugin.getDiscoveredDevices().values()) discovered_devices.sort(key = lambda d: d.name) return discovered_devices - ## Connect a device selected in the list with the active machine. @pyqtSlot(QObject, name = "associateActiveMachineWithPrinterDevice") def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: + """Connect a device selected in the list with the active machine.""" + self._networkPlugin.associateActiveMachineWithPrinterDevice(device) - ## Callback for when the list of discovered devices in the plugin was changed. def _onDeviceDiscoveryChanged(self) -> None: + """Callback for when the list of discovered devices in the plugin was changed.""" + self.discoveredDevicesChanged.emit() - ## Get the network manager from the plugin. @property def _networkPlugin(self) -> UM3OutputDevicePlugin: + """Get the network manager from the plugin.""" + if not self._network_plugin: output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() network_plugin = output_device_manager.getOutputDevicePlugin("UM3NetworkPrinting") diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index f50bab8a1f..13aa0d7063 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -22,10 +22,12 @@ from .Models.Http.ClusterPrinterStatus import ClusterPrinterStatus from .Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus -## Output device class that forms the basis of Ultimaker networked printer output devices. -# Currently used for local networking and cloud printing using Ultimaker Connect. -# This base class primarily contains all the Qt properties and slots needed for the monitor page to work. class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): + """Output device class that forms the basis of Ultimaker networked printer output devices. + + Currently used for local networking and cloud printing using Ultimaker Connect. + This base class primarily contains all the Qt properties and slots needed for the monitor page to work. + """ META_NETWORK_KEY = "um_network_key" META_CLUSTER_ID = "um_cloud_cluster_id" @@ -85,14 +87,16 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # The job upload progress message modal. self._progress = PrintJobUploadProgressMessage() - ## The IP address of the printer. @pyqtProperty(str, constant=True) def address(self) -> str: + """The IP address of the printer.""" + return self._address - ## The display name of the printer. @pyqtProperty(str, constant=True) def printerTypeName(self) -> str: + """The display name of the printer.""" + return self._printer_type_name # Get all print jobs for this cluster. @@ -157,13 +161,15 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): self._active_printer = printer self.activePrinterChanged.emit() - ## Whether the printer that this output device represents supports print job actions via the local network. @pyqtProperty(bool, constant=True) def supportsPrintJobActions(self) -> bool: + """Whether the printer that this output device represents supports print job actions via the local network.""" + return True - ## Set the remote print job state. def setJobState(self, print_job_uuid: str, state: str) -> None: + """Set the remote print job state.""" + raise NotImplementedError("setJobState must be implemented") @pyqtSlot(str, name="sendJobToTop") @@ -210,11 +216,13 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): self._checkStillConnected() super()._update() - ## Check if we're still connected by comparing the last timestamps for network response and the current time. - # This implementation is similar to the base NetworkedPrinterOutputDevice, but is tweaked slightly. - # Re-connecting is handled automatically by the output device managers in this plugin. - # TODO: it would be nice to have this logic in the managers, but connecting those with signals causes crashes. def _checkStillConnected(self) -> None: + """Check if we're still connected by comparing the last timestamps for network response and the current time. + + This implementation is similar to the base NetworkedPrinterOutputDevice, but is tweaked slightly. + Re-connecting is handled automatically by the output device managers in this plugin. + TODO: it would be nice to have this logic in the managers, but connecting those with signals causes crashes. + """ time_since_last_response = time() - self._time_of_last_response if time_since_last_response > self.NETWORK_RESPONSE_CONSIDER_OFFLINE: self.setConnectionState(ConnectionState.Closed) @@ -223,9 +231,11 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): elif self.connectionState == ConnectionState.Closed: self._reconnectForActiveMachine() - ## Reconnect for the active output device. - # Does nothing if the device is not meant for the active machine. def _reconnectForActiveMachine(self) -> None: + """Reconnect for the active output device. + + Does nothing if the device is not meant for the active machine. + """ active_machine = CuraApplication.getInstance().getGlobalContainerStack() if not active_machine: return @@ -281,16 +291,19 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): self.printersChanged.emit() self._checkIfClusterHost() - ## Check is this device is a cluster host and takes the needed actions when it is not. def _checkIfClusterHost(self): + """Check is this device is a cluster host and takes the needed actions when it is not.""" + if len(self._printers) < 1 and self.isConnected(): NotClusterHostMessage(self).show() self.close() CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(self.key) - ## Updates the local list of print jobs with the list received from the cluster. - # \param remote_jobs: The print jobs received from the cluster. def _updatePrintJobs(self, remote_jobs: List[ClusterPrintJobStatus]) -> None: + """Updates the local list of print jobs with the list received from the cluster. + + :param remote_jobs: The print jobs received from the cluster. + """ self._responseReceived() # Keep track of the new print jobs to show. @@ -321,9 +334,11 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): self._print_jobs = new_print_jobs self.printJobsChanged.emit() - ## Create a new print job model based on the remote status of the job. - # \param remote_job: The remote print job data. def _createPrintJobModel(self, remote_job: ClusterPrintJobStatus) -> UM3PrintJobOutputModel: + """Create a new print job model based on the remote status of the job. + + :param remote_job: The remote print job data. + """ model = remote_job.createOutputModel(ClusterOutputController(self)) if remote_job.printer_uuid: self._updateAssignedPrinter(model, remote_job.printer_uuid) @@ -333,16 +348,18 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): model.loadPreviewImageFromUrl(remote_job.preview_url) return model - ## Updates the printer assignment for the given print job model. def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None: + """Updates the printer assignment for the given print job model.""" + printer = next((p for p in self._printers if printer_uuid == p.key), None) if not printer: return printer.updateActivePrintJob(model) model.updateAssignedPrinter(printer) - ## Load Monitor tab QML. def _loadMonitorTab(self) -> None: + """Load Monitor tab QML.""" + plugin_registry = CuraApplication.getInstance().getPluginRegistry() if not plugin_registry: Logger.log("e", "Could not get plugin registry") diff --git a/plugins/USBPrinting/AutoDetectBaudJob.py b/plugins/USBPrinting/AutoDetectBaudJob.py index 36e9637c47..04fe64baaa 100644 --- a/plugins/USBPrinting/AutoDetectBaudJob.py +++ b/plugins/USBPrinting/AutoDetectBaudJob.py @@ -18,7 +18,7 @@ class AutoDetectBaudJob(Job): def __init__(self, serial_port: int) -> None: super().__init__() self._serial_port = serial_port - self._all_baud_rates = [115200, 250000, 500000, 230400, 57600, 38400, 19200, 9600] + self._all_baud_rates = [115200, 250000, 500000, 230400, 76800, 57600, 38400, 19200, 9600] def run(self) -> None: Logger.log("d", "Auto detect baud rate started.") diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 6be3827e5e..f8d344839c 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -58,7 +58,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._baud_rate = baud_rate - self._all_baud_rates = [115200, 250000, 500000, 230400, 57600, 38400, 19200, 9600] + self._all_baud_rates = [115200, 250000, 500000, 230400, 76800, 57600, 38400, 19200, 9600] # Instead of using a timer, we really need the update to be as a thread, as reading from serial can block. self._update_thread = Thread(target = self._update, daemon = True, name = "USBPrinterUpdate") @@ -110,20 +110,22 @@ class USBPrinterOutputDevice(PrinterOutputDevice): application = CuraApplication.getInstance() application.triggerNextExitCheck() - ## Reset USB device settings - # def resetDeviceSettings(self) -> None: + """Reset USB device settings""" + self._firmware_name = None - ## Request the current scene to be sent to a USB-connected printer. - # - # \param nodes A collection of scene nodes to send. This is ignored. - # \param file_name A suggestion for a file name to write. - # \param filter_by_machine Whether to filter MIME types by machine. This - # is ignored. - # \param kwargs Keyword arguments. def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: + """Request the current scene to be sent to a USB-connected printer. + + :param nodes: A collection of scene nodes to send. This is ignored. + :param file_name: A suggestion for a file name to write. + :param filter_by_machine: Whether to filter MIME types by machine. This + is ignored. + :param kwargs: Keyword arguments. + """ + if self._is_printing: message = Message(text = catalog.i18nc("@message", "A print is still in progress. Cura cannot start another print via USB until the previous print has completed."), title = catalog.i18nc("@message", "Print in Progress")) message.show() @@ -144,9 +146,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._printGCode(gcode_textio.getvalue()) - ## Start a print based on a g-code. - # \param gcode The g-code to print. def _printGCode(self, gcode: str): + """Start a print based on a g-code. + + :param gcode: The g-code to print. + """ self._gcode.clear() self._paused = False @@ -219,8 +223,9 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._update_thread = Thread(target=self._update, daemon=True, name = "USBPrinterUpdate") self._serial = None - ## Send a command to printer. def sendCommand(self, command: Union[str, bytes]): + """Send a command to printer.""" + if not self._command_received.is_set(): self._command_queue.put(command) else: diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index a0585adf51..c5a017db7f 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -20,9 +20,10 @@ from . import USBPrinterOutputDevice i18n_catalog = i18nCatalog("cura") -## Manager class that ensures that an USBPrinterOutput device is created for every connected USB printer. @signalemitter class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): + """Manager class that ensures that an USBPrinterOutput device is created for every connected USB printer.""" + addUSBOutputDeviceSignal = Signal() progressChanged = pyqtSignal() @@ -85,8 +86,9 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): self._addRemovePorts(port_list) time.sleep(5) - ## Helper to identify serial ports (and scan for them) def _addRemovePorts(self, serial_ports): + """Helper to identify serial ports (and scan for them)""" + # First, find and add all new or changed keys for serial_port in list(serial_ports): if serial_port not in self._serial_port_list: @@ -98,16 +100,19 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): if port not in self._serial_port_list: device.close() - ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addOutputDevice(self, serial_port): + """Because the model needs to be created in the same thread as the QMLEngine, we use a signal.""" + device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port) device.connectionStateChanged.connect(self._onConnectionStateChanged) self._usb_output_devices[serial_port] = device device.connect() - ## Create a list of serial ports on the system. - # \param only_list_usb If true, only usb ports are listed def getSerialPortList(self, only_list_usb = False): + """Create a list of serial ports on the system. + + :param only_list_usb: If true, only usb ports are listed + """ base_list = [] for port in serial.tools.list_ports.comports(): if not isinstance(port, tuple): diff --git a/plugins/USBPrinting/avr_isp/intelHex.py b/plugins/USBPrinting/avr_isp/intelHex.py index 671f1788f7..fc410882dd 100644 --- a/plugins/USBPrinting/avr_isp/intelHex.py +++ b/plugins/USBPrinting/avr_isp/intelHex.py @@ -31,7 +31,7 @@ def readHex(filename): check_sum &= 0xFF if check_sum != 0: raise Exception("Checksum error in hex file: " + line) - + if rec_type == 0:#Data record while len(data) < addr + rec_len: data.append(0) diff --git a/plugins/USBPrinting/avr_isp/ispBase.py b/plugins/USBPrinting/avr_isp/ispBase.py index 077cfc36e6..9ba3a65b47 100644 --- a/plugins/USBPrinting/avr_isp/ispBase.py +++ b/plugins/USBPrinting/avr_isp/ispBase.py @@ -22,7 +22,7 @@ class IspBase(): if not self.chip: raise IspError("Chip with signature: " + str(self.getSignature()) + "not found") self.chipErase() - + Logger.log("d", "Flashing %i bytes", len(flash_data)) self.writeFlash(flash_data) Logger.log("d", "Verifying %i bytes", len(flash_data)) diff --git a/plugins/USBPrinting/avr_isp/stk500v2.py b/plugins/USBPrinting/avr_isp/stk500v2.py index dbfc8dc756..2b1a86155c 100644 --- a/plugins/USBPrinting/avr_isp/stk500v2.py +++ b/plugins/USBPrinting/avr_isp/stk500v2.py @@ -56,7 +56,7 @@ class Stk500v2(ispBase.IspBase): self.close() raise self.serial.timeout = 5 - + def close(self): if self.serial is not None: self.serial.close() diff --git a/plugins/UltimakerMachineActions/BedLevelMachineAction.py b/plugins/UltimakerMachineActions/BedLevelMachineAction.py index 818ad0e4f0..f76e0c6746 100644 --- a/plugins/UltimakerMachineActions/BedLevelMachineAction.py +++ b/plugins/UltimakerMachineActions/BedLevelMachineAction.py @@ -14,9 +14,12 @@ from UM.Logger import Logger catalog = i18nCatalog("cura") -## A simple action to handle manual bed leveling procedure for printers that don't have it on the firmware. -# This is currently only used by the Ultimaker Original+ class BedLevelMachineAction(MachineAction): + """A simple action to handle manual bed leveling procedure for printers that don't have it on the firmware. + + This is currently only used by the Ultimaker Original+ + """ + def __init__(self): super().__init__("BedLevel", catalog.i18nc("@action", "Level build plate")) self._qml_url = "BedLevelMachineAction.qml" diff --git a/plugins/UltimakerMachineActions/UMOUpgradeSelection.py b/plugins/UltimakerMachineActions/UMOUpgradeSelection.py index f6275e5b56..62eab75986 100644 --- a/plugins/UltimakerMachineActions/UMOUpgradeSelection.py +++ b/plugins/UltimakerMachineActions/UMOUpgradeSelection.py @@ -11,9 +11,12 @@ catalog = i18nCatalog("cura") from cura.Settings.CuraStackBuilder import CuraStackBuilder -## The Ultimaker Original can have a few revisions & upgrades. This action helps with selecting them, so they are added -# as a variant. + class UMOUpgradeSelection(MachineAction): + """The Ultimaker Original can have a few revisions & upgrades. + This action helps with selecting them, so they are added as a variant. + """ + def __init__(self): super().__init__("UMOUpgradeSelection", catalog.i18nc("@action", "Select upgrades")) self._qml_url = "UMOUpgradeSelectionMachineAction.qml" diff --git a/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py b/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py index fdca7d15df..85a7e3135a 100644 --- a/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py +++ b/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py @@ -34,8 +34,9 @@ class VersionUpgrade35to40(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades Preferences to have the new version number. def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades Preferences to have the new version number.""" + parser = configparser.ConfigParser(interpolation=None) parser.read_string(serialized) @@ -48,9 +49,9 @@ class VersionUpgrade35to40(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades instance containers to have the new version - # number. def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades instance containers to have the new version number.""" + parser = configparser.ConfigParser(interpolation=None) parser.read_string(serialized) diff --git a/plugins/VersionUpgrade/VersionUpgrade40to41/VersionUpgrade40to41.py b/plugins/VersionUpgrade/VersionUpgrade40to41/VersionUpgrade40to41.py index 1cccbef7da..c1df23e040 100644 --- a/plugins/VersionUpgrade/VersionUpgrade40to41/VersionUpgrade40to41.py +++ b/plugins/VersionUpgrade/VersionUpgrade40to41/VersionUpgrade40to41.py @@ -20,12 +20,14 @@ _renamed_quality_profiles = { } # type: Dict[str, str] -## Upgrades configurations from the state they were in at version 4.0 to the -# state they should be in at version 4.1. class VersionUpgrade40to41(VersionUpgrade): - ## Upgrades instance containers to have the new version - # number. + """Upgrades configurations from the state they were in at version 4.0 to the + state they should be in at version 4.1. + """ + def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades instance containers to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -46,8 +48,9 @@ class VersionUpgrade40to41(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades Preferences to have the new version number. def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades Preferences to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -66,8 +69,9 @@ class VersionUpgrade40to41(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades stacks to have the new version number. def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades stacks to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) diff --git a/plugins/VersionUpgrade/VersionUpgrade41to42/VersionUpgrade41to42.py b/plugins/VersionUpgrade/VersionUpgrade41to42/VersionUpgrade41to42.py index 43d8561567..0741312011 100644 --- a/plugins/VersionUpgrade/VersionUpgrade41to42/VersionUpgrade41to42.py +++ b/plugins/VersionUpgrade/VersionUpgrade41to42/VersionUpgrade41to42.py @@ -214,14 +214,17 @@ _creality_limited_quality_type = { } -## Upgrades configurations from the state they were in at version 4.1 to the -# state they should be in at version 4.2. class VersionUpgrade41to42(VersionUpgrade): - ## Upgrades instance containers to have the new version - # number. - # - # This renames the renamed settings in the containers. + """Upgrades configurations from the state they were in at version 4.1 to the + + state they should be in at version 4.2. + """ + def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades instance containers to have the new version number. + + This renames the renamed settings in the containers. + """ parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ()) parser.read_string(serialized) @@ -257,10 +260,12 @@ class VersionUpgrade41to42(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades Preferences to have the new version number. - # - # This renames the renamed settings in the list of visible settings. def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades Preferences to have the new version number. + + This renames the renamed settings in the list of visible settings. + """ + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -284,8 +289,9 @@ class VersionUpgrade41to42(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades stacks to have the new version number. def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades stacks to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) diff --git a/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py b/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py index b139ede83c..73d6578c9b 100644 --- a/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py +++ b/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py @@ -57,9 +57,11 @@ _renamed_settings = { } # type: Dict[str, str] -## Upgrades configurations from the state they were in at version 4.2 to the -# state they should be in at version 4.3. class VersionUpgrade42to43(VersionUpgrade): + """Upgrades configurations from the state they were in at version 4.2 to the + + state they should be in at version 4.3. + """ def upgradePreferences(self, serialized: str, filename: str): parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -77,16 +79,16 @@ class VersionUpgrade42to43(VersionUpgrade): parser["general"]["visible_settings"] = ";".join(all_setting_keys) parser["metadata"]["setting_version"] = "9" - + result = io.StringIO() parser.write(result) return [filename], [result.getvalue()] - ## Upgrades instance containers to have the new version - # number. - # - # This renames the renamed settings in the containers. def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades instance containers to have the new version number. + + This renames the renamed settings in the containers. + """ parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ()) parser.read_string(serialized) @@ -111,8 +113,9 @@ class VersionUpgrade42to43(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades stacks to have the new version number. def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades stacks to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) diff --git a/plugins/VersionUpgrade/VersionUpgrade43to44/VersionUpgrade43to44.py b/plugins/VersionUpgrade/VersionUpgrade43to44/VersionUpgrade43to44.py index b5825af62e..ec0a767105 100644 --- a/plugins/VersionUpgrade/VersionUpgrade43to44/VersionUpgrade43to44.py +++ b/plugins/VersionUpgrade/VersionUpgrade43to44/VersionUpgrade43to44.py @@ -27,10 +27,12 @@ _renamed_container_id_map = { class VersionUpgrade43to44(VersionUpgrade): - ## Upgrades Preferences to have the new version number. - # - # This renames the renamed settings in the list of visible settings. def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades Preferences to have the new version number. + + This renames the renamed settings in the list of visible settings. + """ + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -41,11 +43,11 @@ class VersionUpgrade43to44(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades instance containers to have the new version - # number. - # - # This renames the renamed settings in the containers. def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades instance containers to have the new version number. + + This renames the renamed settings in the containers. + """ parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ()) parser.read_string(serialized) @@ -72,8 +74,9 @@ class VersionUpgrade43to44(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades stacks to have the new version number. def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades stacks to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) diff --git a/plugins/VersionUpgrade/VersionUpgrade44to45/VersionUpgrade44to45.py b/plugins/VersionUpgrade/VersionUpgrade44to45/VersionUpgrade44to45.py index 0747ad280b..00faa216eb 100644 --- a/plugins/VersionUpgrade/VersionUpgrade44to45/VersionUpgrade44to45.py +++ b/plugins/VersionUpgrade/VersionUpgrade44to45/VersionUpgrade44to45.py @@ -122,10 +122,12 @@ class VersionUpgrade44to45(VersionUpgrade): except OSError: # Is a directory, file not found, or insufficient rights. continue - ## Upgrades Preferences to have the new version number. - # - # This renames the renamed settings in the list of visible settings. def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades Preferences to have the new version number. + + This renames the renamed settings in the list of visible settings. + """ + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -136,11 +138,11 @@ class VersionUpgrade44to45(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades instance containers to have the new version - # number. - # - # This renames the renamed settings in the containers. def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades instance containers to have the new version number. + + This renames the renamed settings in the containers. + """ parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ()) parser.read_string(serialized) @@ -166,8 +168,9 @@ class VersionUpgrade44to45(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades stacks to have the new version number. def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades stacks to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) diff --git a/plugins/VersionUpgrade/VersionUpgrade45to46/__init__.py b/plugins/VersionUpgrade/VersionUpgrade45to46/__init__.py index 090f8a109c..e9e835555c 100644 --- a/plugins/VersionUpgrade/VersionUpgrade45to46/__init__.py +++ b/plugins/VersionUpgrade/VersionUpgrade45to46/__init__.py @@ -21,6 +21,15 @@ def getMetaData() -> Dict[str, Any]: ("quality_changes", 4000011): ("quality_changes", 4000013, upgrade.upgradeInstanceContainer), ("quality", 4000011): ("quality", 4000013, upgrade.upgradeInstanceContainer), ("user", 4000011): ("user", 4000013, upgrade.upgradeInstanceContainer), + + # Setting version 12 was also used during the 4.6 beta, but had no changes. + ("preferences", 6000012): ("preferences", 6000013, upgrade.upgradePreferences), + ("machine_stack", 4000012): ("machine_stack", 4000013, upgrade.upgradeStack), + ("extruder_train", 4000012): ("extruder_train", 4000013, upgrade.upgradeStack), + ("definition_changes", 4000012): ("definition_changes", 4000013, upgrade.upgradeInstanceContainer), + ("quality_changes", 4000012): ("quality_changes", 4000013, upgrade.upgradeInstanceContainer), + ("quality", 4000012): ("quality", 4000013, upgrade.upgradeInstanceContainer), + ("user", 4000012): ("user", 4000013, upgrade.upgradeInstanceContainer), }, "sources": { "preferences": { diff --git a/plugins/VersionUpgrade/VersionUpgrade460to462/VersionUpgrade460to462.py b/plugins/VersionUpgrade/VersionUpgrade460to462/VersionUpgrade460to462.py index b5a952e418..1aa5e1b2da 100644 --- a/plugins/VersionUpgrade/VersionUpgrade460to462/VersionUpgrade460to462.py +++ b/plugins/VersionUpgrade/VersionUpgrade460to462/VersionUpgrade460to462.py @@ -90,16 +90,17 @@ class VersionUpgrade460to462(VersionUpgrade): parser_e3["general"]["definition"] = "deltacomb_base_extruder_3" results.append((parser_e2, filename + "_e2_upgrade")) # Hopefully not already taken. results.append((parser_e3, filename + "_e3_upgrade")) - elif parser["general"]["definition"] == "deltacomb": # On the global stack OR the per-extruder user container. + elif parser["general"]["definition"] == "deltacomb": # On the global stack, the per-extruder user container OR the per-extruder quality changes container. parser["general"]["definition"] = "deltacomb_dc20" - if "metadata" in parser and "extruder" in parser["metadata"]: # Per-extruder user container. + if "metadata" in parser and ("extruder" in parser["metadata"] or "position" in parser["metadata"]): # Per-extruder user container or quality changes container. parser_e2 = configparser.ConfigParser(interpolation = None) parser_e3 = configparser.ConfigParser(interpolation = None) parser_e2.read_dict(parser) parser_e3.read_dict(parser) - parser_e2["metadata"]["extruder"] += "_e2_upgrade" - parser_e3["metadata"]["extruder"] += "_e3_upgrade" + if "extruder" in parser["metadata"]: + parser_e2["metadata"]["extruder"] += "_e2_upgrade" + parser_e3["metadata"]["extruder"] += "_e3_upgrade" results.append((parser_e2, filename + "_e2_upgrade")) results.append((parser_e3, filename + "_e3_upgrade")) diff --git a/plugins/VersionUpgrade/VersionUpgrade462to47/VersionUpgrade462to47.py b/plugins/VersionUpgrade/VersionUpgrade462to47/VersionUpgrade462to47.py index c340fd0c72..7bee545c16 100644 --- a/plugins/VersionUpgrade/VersionUpgrade462to47/VersionUpgrade462to47.py +++ b/plugins/VersionUpgrade/VersionUpgrade462to47/VersionUpgrade462to47.py @@ -2,10 +2,17 @@ # Cura is released under the terms of the LGPLv3 or higher. import configparser -from typing import Tuple, List +from typing import Tuple, List, Dict import io from UM.VersionUpgrade import VersionUpgrade + +# Renamed definition files +_RENAMED_DEFINITION_DICT = { + "dagoma_discoeasy200": "dagoma_discoeasy200_bicolor", +} # type: Dict[str, str] + + class VersionUpgrade462to47(VersionUpgrade): def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: """ @@ -58,6 +65,23 @@ class VersionUpgrade462to47(VersionUpgrade): maximum_deviation = "=(" + maximum_deviation + ") / 2" parser["values"]["meshfix_maximum_deviation"] = maximum_deviation + # Ironing inset is now based on the flow-compensated line width to make the setting have a more logical UX. + # Adjust so that the actual print result remains the same. + if "ironing_inset" in parser["values"]: + ironing_inset = parser["values"]["ironing_inset"] + if ironing_inset.startswith("="): + ironing_inset = ironing_inset[1:] + if "ironing_pattern" in parser["values"] and parser["values"]["ironing_pattern"] == "concentric": + correction = " + ironing_line_spacing - skin_line_width * (1.0 + ironing_flow / 100) / 2" + else: # If ironing_pattern doesn't exist, it means the default (zigzag) is selected + correction = " + skin_line_width * (1.0 - ironing_flow / 100) / 2" + ironing_inset = "=(" + ironing_inset + ")" + correction + parser["values"]["ironing_inset"] = ironing_inset + + # Check renamed definitions + if "definition" in parser["general"] and parser["general"]["definition"] in _RENAMED_DEFINITION_DICT: + parser["general"]["definition"] = _RENAMED_DEFINITION_DICT[parser["general"]["definition"]] + result = io.StringIO() parser.write(result) return [filename], [result.getvalue()] @@ -88,6 +112,25 @@ class VersionUpgrade462to47(VersionUpgrade): 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.read_string(script_str) + + # Unify all Pause at Height + script_id = script_parser.sections()[0] + if script_id in ["BQ_PauseAtHeight", "PauseAtHeightRepRapFirmwareDuet", "PauseAtHeightforRepetier"]: + script_settings = script_parser.items(script_id) + script_settings.append(("pause_method", { + "BQ_PauseAtHeight": "bq", + "PauseAtHeightforRepetier": "repetier", + "PauseAtHeightRepRapFirmwareDuet": "reprap" + }[script_id])) + + # Since we cannot rename a section, we remove the original section and create a new section with the new script id. + script_parser.remove_section(script_id) + script_id = "PauseAtHeight" + script_parser.add_section(script_id) + for setting_tuple in script_settings: + script_parser.set(script_id, setting_tuple[0], setting_tuple[1]) + + # Update redo_layers to redo_layer if "PauseAtHeight" in script_parser: if "redo_layers" in script_parser["PauseAtHeight"]: script_parser["PauseAtHeight"]["redo_layer"] = str(int(script_parser["PauseAtHeight"]["redo_layers"]) > 0) @@ -98,7 +141,9 @@ class VersionUpgrade462to47(VersionUpgrade): script_str = script_str.replace("\\\\", r"\\\\").replace("\n", r"\\\n") # Escape newlines because configparser sees those as section delimiters. new_scripts_entries.append(script_str) parser["metadata"]["post_processing_scripts"] = "\n".join(new_scripts_entries) - + # check renamed definition + if parser.has_option("containers", "7") and parser["containers"]["7"] in _RENAMED_DEFINITION_DICT: + parser["containers"]["7"] = _RENAMED_DEFINITION_DICT[parser["containers"]["7"]] result = io.StringIO() parser.write(result) return [filename], [result.getvalue()] diff --git a/plugins/XRayView/XRayView.py b/plugins/XRayView/XRayView.py index 0144f4c176..be4fe5ea76 100644 --- a/plugins/XRayView/XRayView.py +++ b/plugins/XRayView/XRayView.py @@ -23,8 +23,9 @@ from cura.Scene.ConvexHullNode import ConvexHullNode from cura import XRayPass -## View used to display a see-through version of objects with errors highlighted. class XRayView(CuraView): + """View used to display a see-through version of objects with errors highlighted.""" + def __init__(self): super().__init__(parent = None, use_empty_menu_placeholder = True) diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 4e0d1e09f5..6fe4d4242b 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -25,8 +25,9 @@ except (ImportError, SystemError): import XmlMaterialValidator # type: ignore # This fixes the tests not being able to import. -## Handles serializing and deserializing material containers from an XML file class XmlMaterialProfile(InstanceContainer): + """Handles serializing and deserializing material containers from an XML file""" + CurrentFdmMaterialVersion = "1.3" Version = 1 @@ -34,17 +35,18 @@ class XmlMaterialProfile(InstanceContainer): super().__init__(container_id, *args, **kwargs) self._inherited_files = [] - ## Translates the version number in the XML files to the setting_version - # metadata entry. - # - # Since the two may increment independently we need a way to say which - # versions of the XML specification are compatible with our setting data - # version numbers. - # - # \param xml_version: The version number found in an XML file. - # \return The corresponding setting_version. @staticmethod def xmlVersionToSettingVersion(xml_version: str) -> int: + """Translates the version number in the XML files to the setting_version metadata entry. + + Since the two may increment independently we need a way to say which + versions of the XML specification are compatible with our setting data + version numbers. + + :param xml_version: The version number found in an XML file. + :return: The corresponding setting_version. + """ + if xml_version == "1.3": return CuraApplication.SettingVersion return 0 # Older than 1.3. @@ -52,15 +54,17 @@ class XmlMaterialProfile(InstanceContainer): def getInheritedFiles(self): return self._inherited_files - ## Overridden from InstanceContainer - # set the meta data for all machine / variant combinations - # - # The "apply_to_all" flag indicates whether this piece of metadata should be applied to all material containers - # or just this specific container. - # For example, when you change the material name, you want to apply it to all its derived containers, but for - # some specific settings, they should only be applied to a machine/variant-specific container. - # def setMetaDataEntry(self, key, value, apply_to_all = True): + """set the meta data for all machine / variant combinations + + The "apply_to_all" flag indicates whether this piece of metadata should be applied to all material containers + or just this specific container. + For example, when you change the material name, you want to apply it to all its derived containers, but for + some specific settings, they should only be applied to a machine/variant-specific container. + + Overridden from InstanceContainer + """ + registry = ContainerRegistry.getInstance() if registry.isReadOnly(self.getId()): Logger.log("w", "Can't change metadata {key} of material {material_id} because it's read-only.".format(key = key, material_id = self.getId())) @@ -89,10 +93,13 @@ class XmlMaterialProfile(InstanceContainer): for k, v in new_setting_values_dict.items(): self.setProperty(k, "value", v) - ## Overridden from InstanceContainer, similar to setMetaDataEntry. - # without this function the setName would only set the name of the specific nozzle / material / machine combination container - # The function is a bit tricky. It will not set the name of all containers if it has the correct name itself. def setName(self, new_name): + """Overridden from InstanceContainer, similar to setMetaDataEntry. + + without this function the setName would only set the name of the specific nozzle / material / machine combination container + The function is a bit tricky. It will not set the name of all containers if it has the correct name itself. + """ + registry = ContainerRegistry.getInstance() if registry.isReadOnly(self.getId()): return @@ -110,8 +117,9 @@ class XmlMaterialProfile(InstanceContainer): for container in containers: container.setName(new_name) - ## Overridden from InstanceContainer, to set dirty to base file as well. def setDirty(self, dirty): + """Overridden from InstanceContainer, to set dirty to base file as well.""" + super().setDirty(dirty) base_file = self.getMetaDataEntry("base_file", None) registry = ContainerRegistry.getInstance() @@ -120,10 +128,12 @@ class XmlMaterialProfile(InstanceContainer): if containers: containers[0].setDirty(dirty) - ## Overridden from InstanceContainer - # base file: common settings + supported machines - # machine / variant combination: only changes for itself. def serialize(self, ignored_metadata_keys: Optional[Set[str]] = None): + """Overridden from InstanceContainer + + base file: common settings + supported machines + machine / variant combination: only changes for itself. + """ registry = ContainerRegistry.getInstance() base_file = self.getMetaDataEntry("base_file", "") @@ -455,7 +465,7 @@ class XmlMaterialProfile(InstanceContainer): return "materials" @classmethod - def getVersionFromSerialized(cls, serialized: str) -> Optional[int]: + def getVersionFromSerialized(cls, serialized: str) -> int: data = ET.fromstring(serialized) version = XmlMaterialProfile.Version @@ -467,8 +477,9 @@ class XmlMaterialProfile(InstanceContainer): return version * 1000000 + setting_version - ## Overridden from InstanceContainer def deserialize(self, serialized, file_name = None): + """Overridden from InstanceContainer""" + containers_to_add = [] # update the serialized data first from UM.Settings.Interfaces import ContainerInterface @@ -1061,12 +1072,13 @@ class XmlMaterialProfile(InstanceContainer): id_list = list(id_list) return id_list - ## Gets a mapping from product names in the XML files to their definition - # IDs. - # - # This loads the mapping from a file. @classmethod def getProductIdMap(cls) -> Dict[str, List[str]]: + """Gets a mapping from product names in the XML files to their definition IDs. + + This loads the mapping from a file. + """ + plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath("XmlMaterialProfile")) product_to_id_file = os.path.join(plugin_path, "product_to_id.json") with open(product_to_id_file, encoding = "utf-8") as f: @@ -1076,13 +1088,15 @@ class XmlMaterialProfile(InstanceContainer): #However it is not always loaded with that default; this mapping is also used in serialize() without that default. return product_to_id_map - ## Parse the value of the "material compatible" property. @staticmethod def _parseCompatibleValue(value: str): + """Parse the value of the "material compatible" property.""" + return value in {"yes", "unknown"} - ## Small string representation for debugging. def __str__(self): + """Small string representation for debugging.""" + return "".format(my_id = self.getId(), name = self.getName(), base_file = self.getMetaDataEntry("base_file")) _metadata_tags_that_have_cura_namespace = {"pva_compatible", "breakaway_compatible"} @@ -1132,8 +1146,10 @@ class XmlMaterialProfile(InstanceContainer): "cura": "http://www.ultimaker.com/cura" } -## Helper function for pretty-printing XML because ETree is stupid + def _indent(elem, level = 0): + """Helper function for pretty-printing XML because ETree is stupid""" + i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): diff --git a/plugins/XmlMaterialProfile/XmlMaterialValidator.py b/plugins/XmlMaterialProfile/XmlMaterialValidator.py index a23022854b..c191577a88 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialValidator.py +++ b/plugins/XmlMaterialProfile/XmlMaterialValidator.py @@ -3,11 +3,14 @@ from typing import Any, Dict -## Makes sure that the required metadata is present for a material. + class XmlMaterialValidator: - ## Makes sure that the required metadata is present for a material. + """Makes sure that the required metadata is present for a material.""" + @classmethod def validateMaterialMetaData(cls, validation_metadata: Dict[str, Any]): + """Makes sure that the required metadata is present for a material.""" + if validation_metadata.get("GUID") is None: return "Missing GUID" diff --git a/resources/definitions/I3MetalMotion.def.json b/resources/definitions/I3MetalMotion.def.json new file mode 100644 index 0000000000..a9d55969bb --- /dev/null +++ b/resources/definitions/I3MetalMotion.def.json @@ -0,0 +1,33 @@ +{ + "version": 2, + "name": "I3 Metal Motion", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Peter Felecan", + "manufacturer": "eMotionTech", + "file_formats": "text/x-gcode", + "has_materials": true, + "preferred_material": "emotiontech_pla", + "machine_extruder_trains": + { + "0": "I3MetalMotion_extruder_0" + } + }, + + "overrides": { + "machine_name": { "default_value": "I3MetalMotion" }, + "machine_heated_bed": { "default_value": true }, + "machine_width": { "default_value": 200 }, + "machine_height": { "default_value": 200 }, + "machine_depth": { "default_value": 200 }, + "machine_center_is_zero": { "default_value": false }, + "machine_gcode_flavor": { "default_value": "RepRap (RepRap)" }, + "machine_start_gcode": { + "default_value": "G21 ; set units to millimeters\nG90 ; use absolute positioning\nM82 ; absolute extrusion mode\nM104 S{material_print_temperature_layer_0} ; set extruder temp\nM140 S{material_bed_temperature_layer_0} ; set bed temp\nM190 S{material_bed_temperature_layer_0} ; wait for bed temp\nM109 S{material_print_temperature_layer_0} ; wait for extruder temp\nG28 W ; home all\nG92 E0.0 ; reset extruder distance position\nG1 Y-3.0 F1000.0 ; go outside print area\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E21.5 F1000.0 ; intro line\nG92 E0.0 ; reset extruder distance position" + }, + "machine_end_gcode": { + "default_value": "G28 Z\nG28 X\nG28 Y\nM107 ; Turn off the fan\nG91; Relative positioning\nG1 E-1 ; reduce filament pressure\nM104 T0 S0\nG90 ; Absolute positioning\nG92 E0 ; Reset extruder position\nM140 S0 ; Disable heated bed\nM84 ; Turn the steppers off" + } + } +} diff --git a/resources/definitions/SV01.def.json b/resources/definitions/SV01.def.json new file mode 100644 index 0000000000..fb410151a9 --- /dev/null +++ b/resources/definitions/SV01.def.json @@ -0,0 +1,70 @@ +{ + "version": 2, + "name": "Sovol-SV01", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Sovol", + "manufacturer": "Sovol 3D", + "file_formats": "text/x-gcode", + "has_variants": false, + "has_machine_quality": false, + "preferred_quality_type": "draft", + "machine_extruder_trains": { + "0": "SV01_extruder_0" + } + }, + + "overrides": { + "machine_name": { "default_value": "SV01" }, + "machine_extruder_count": { "default_value": 1 }, + "machine_width": { "default_value": 280 }, + "machine_depth": { "default_value": 260 }, + "machine_height": { "default_value": 300 }, + "machine_max_feedrate_x": { "value": 500 }, + "machine_max_feedrate_y": { "value": 500 }, + "machine_max_feedrate_z": { "value": 10 }, + "machine_max_feedrate_e": { "value": 50 }, + "machine_max_acceleration_x": { "value": 500 }, + "machine_max_acceleration_y": { "value": 500 }, + "machine_max_acceleration_z": { "value": 100 }, + "machine_max_acceleration_e": { "value": 5000 }, + "machine_acceleration": { "value": 500 }, + "machine_max_jerk_xy": { "value": 10 }, + "machine_max_jerk_z": { "value": 0.4 }, + "machine_max_jerk_e": { "value": 5 }, + "machine_heated_bed": { "default_value": true }, + "material_diameter": { "default_value": 1.75 }, + "acceleration_print": { "value": 500 }, + "acceleration_travel": { "value": 500 }, + "acceleration_travel_layer_0": { "value": "acceleration_travel" }, + "acceleration_roofing": { "enabled": "acceleration_enabled and roofing_layer_count > 0 and top_layers > 0" }, + "jerk_print": { "value": 8 }, + "jerk_travel": { "value": "jerk_print" }, + "jerk_travel_layer_0": { "value": "jerk_travel" }, + "acceleration_enabled": { "value": false }, + "jerk_enabled": { "value": false }, + "speed_print": { "value": 50.0 } , + "speed_infill": { "value": "speed_print" }, + "skirt_brim_speed": { "value": "speed_layer_0" }, + "line_width": { "value": "machine_nozzle_size" }, + "optimize_wall_printing_order": { "value": "True" }, + "material_initial_print_temperature": { "value": "material_print_temperature" }, + "material_final_print_temperature": { "value": "material_print_temperature" }, + "material_flow": { "value": 100 }, + "z_seam_type": { "value": "'back'" }, + "z_seam_corner": { "value": "'z_seam_corner_weighted'" }, + "infill_sparse_density": { "value": "20" }, + "infill_pattern": { "value": "'lines'" }, + "infill_before_walls": { "value": false }, + "infill_overlap": { "value": 30.0 }, + "skin_overlap": { "value": 10.0 }, + "infill_wipe_dist": { "value": 0.0 }, + "wall_0_wipe_dist": { "value": 0.0 }, + "retraction_amount": { "default_value": 3}, + "retraction_speed": { "default_value": 50}, + "adhesion_type": { "value": "'skirt'" }, + "machine_start_gcode": { "default_value": "M201 X500.00 Y500.00 Z100.00 E5000.00 ;Setup machine max acceleration\nM203 X500.00 Y500.00 Z10.00 E50.00 ;Setup machine max feedrate\nM204 P500.00 R1000.00 T500.00 ;Setup Print/Retract/Travel acceleration\nM205 X8.00 Y8.00 Z0.40 E5.00 ;Setup Jerk\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\n\nG28 ;Home\n\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\n" }, + "machine_end_gcode": { "default_value": "G91 ;Relative positioning\nG1 E-2 F2700 ;Retract a bit\nG1 E-2 Z0.2 F2400 ;Retract and raise Z\nG1 X0 Y240 F3000 ;Wipe out\nG1 Z10 ;Raise Z more\nG90 ;Absolute positionning\n\nG1 X0 Y{machine_depth} ;Present print\nM106 S0 ;Turn-off fan\nM104 S0 ;Turn-off hotend\nM140 S0 ;Turn-off bed\n\nM84 X Y E ;Disable all steppers but Z\n" } + } +} \ No newline at end of file diff --git a/resources/definitions/SV02.def.json b/resources/definitions/SV02.def.json new file mode 100644 index 0000000000..067422354e --- /dev/null +++ b/resources/definitions/SV02.def.json @@ -0,0 +1,86 @@ +{ + "version": 2, + "name": "Sovol-SV02", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Sovol", + "manufacturer": "Sovol 3D", + "file_formats": "text/x-gcode", + "has_variants": false, + "has_machine_quality": false, + "preferred_quality_type": "draft", + "machine_extruder_trains": { + "0": "SV02_extruder_0", + "1": "SV02_extruder_1" + } + }, + + "overrides": { + "machine_name": { "default_value": "SV02" }, + "machine_extruder_count": { "default_value": 2 }, + "machine_heated_bed": { "default_value": true }, + "machine_width": { "default_value": 300 }, + "machine_depth": { "default_value": 250 }, + "machine_height": { "default_value": 300 }, + "machine_center_is_zero": { "default_value": false }, + "retraction_amount": { "default_value": 5}, + "retraction_speed": { "default_value": 50}, + "gantry_height": { "value": "30" }, + "speed_print": { "default_value": 50 }, + "material_print_temperature": { "value": 195 }, + "material_print_temperature_layer_0": { "value": "material_print_temperature" }, + "material_initial_print_temperature": { "value": "material_print_temperature" }, + "material_final_print_temperature": { "value": 195 }, + "machine_max_feedrate_x": { "value": 500 }, + "machine_max_feedrate_y": { "value": 500 }, + "machine_max_feedrate_z": { "value": 10 }, + "machine_max_feedrate_e": { "value": 50 }, + "machine_max_acceleration_x": { "value": 500 }, + "machine_max_acceleration_y": { "value": 500 }, + "machine_max_acceleration_z": { "value": 100 }, + "machine_max_acceleration_e": { "value": 500 }, + "machine_acceleration": { "value": 500 }, + "machine_max_jerk_xy": { "value": 8 }, + "machine_max_jerk_z": { "value": 0.4 }, + "machine_max_jerk_e": { "value": 5 }, + "machine_heated_bed": { "default_value": true }, + "material_diameter": { "default_value": 1.75 }, + "infill_overlap": { "default_value": 15 }, + "acceleration_print": { "value": 500 }, + "acceleration_travel": { "value": 500 }, + "acceleration_travel_layer_0": { "value": "acceleration_travel" }, + "jerk_print": { "value": 8 }, + "jerk_travel": { "value": "jerk_print" }, + "jerk_travel_layer_0": { "value": "jerk_travel" }, + "acceleration_enabled": { "value": false }, + "jerk_enabled": { "value": false }, + "machine_max_jerk_xy": { "default_value": 5.0 }, + "machine_max_jerk_z": { "default_value": 0.4 }, + "machine_max_jerk_e": { "default_value": 5.0 }, + "prime_tower_position_x": { "value": "240" }, + "prime_tower_position_y": { "value": "190" }, + "prime_tower_size": { "value": "30" }, + "prime_tower_wipe_enabled": { "default_value": true }, + "prime_tower_min_volume": { "value": "((resolveOrValue('prime_tower_size') * 0.5) ** 2 * 3.14159 * resolveOrValue('layer_height'))/2"}, + "travel_retract_before_outer_wall": { "default_value": true }, + "infill_sparse_density": { "value": "15" }, + "infill_pattern": { "value": "'lines'" }, + "infill_before_walls": { "value": false }, + "infill_overlap": { "value": 30.0 }, + "skin_overlap": { "value": 10.0 }, + "infill_wipe_dist": { "value": 0.0 }, + "wall_0_wipe_dist": { "value": 0.0 }, + "adhesion_type": { "value": "'skirt'" }, + "brim_replaces_support": { "value": false }, + "skirt_gap": { "value": 2 }, + "skirt_line_count": { "value": 3 }, + "adhesion_extruder_nr": { "value": 1 }, + "brim_width": { "value": 4 }, + "coasting_enable": { "default_value": true }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_start_gcode": { "default_value": "G21 ;metric values\nG28 ;home all\nG90 ;absolute positioning\nM107 ;start with the fan off\nG1 F2400 Z15.0 ;raise the nozzle 15mm\nM109 S{material_print_temperature} ;Set Extruder Temperature and Wait\nM190 S{material_bed_temperature}; Wait for bed temperature to reach target temp\nT0 ;Switch to Extruder 1\nG1 F3000 X5 Y10 Z0.2 ;move to prime start position\nG92 E0 ;reset extrusion distance\nG1 F600 X160 E5 ;prime nozzle in a line\nG1 F5000 X180 ;quick wipe\nG92 E0 ;reset extrusion distance" }, + "machine_end_gcode": { "default_value": "M104 S0 ;hotend off\nM140 S0 ;bed off\nG92 E0\nG1 F2000 E-100 ;retract filament 100mm\nG92 E0\nG1 F3000 X0 Y240 ;move bed for easy part removal\nM84 ;disable steppers" }, + "top_bottom_thickness": { "default_value": 1 } + } +} diff --git a/resources/definitions/atmat_asterion.def.json b/resources/definitions/atmat_asterion.def.json new file mode 100644 index 0000000000..402dc51c8c --- /dev/null +++ b/resources/definitions/atmat_asterion.def.json @@ -0,0 +1,19 @@ +{ + "name": "Asterion", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "platform": "atmat_asterion_platform.stl", + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Asterion" }, + "machine_width": { "default_value": 500 }, + "machine_depth": { "default_value": 500 }, + "machine_height": { "default_value": 600 } + + } +} diff --git a/resources/definitions/atmat_asterion_ht.def.json b/resources/definitions/atmat_asterion_ht.def.json new file mode 100644 index 0000000000..19c8d1f781 --- /dev/null +++ b/resources/definitions/atmat_asterion_ht.def.json @@ -0,0 +1,20 @@ +{ + "name": "Asterion HT", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "platform": "atmat_asterion_platform.stl", + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Asterion HT" }, + "machine_width": { "default_value": 500 }, + "machine_depth": { "default_value": 500 }, + "machine_height": { "default_value": 600 }, + "material_print_temperature": { "maximum_value_warning": 500 } + + } +} diff --git a/resources/definitions/atmat_galaxy_500.def.json b/resources/definitions/atmat_galaxy_500.def.json new file mode 100644 index 0000000000..f0ed3be841 --- /dev/null +++ b/resources/definitions/atmat_galaxy_500.def.json @@ -0,0 +1,36 @@ +{ + "name": "Galaxy 500", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Galaxy 500" }, + "machine_width": { "default_value": 400 }, + "machine_depth": { "default_value": 400 }, + "machine_height": { "default_value": 500 }, + "acceleration_layer_0": { "value": "250" }, + "acceleration_prime_tower": { "value": "750" }, + "acceleration_print": { "value": "750" }, + "acceleration_travel": { "value": "750" }, + "acceleration_support": { "value": "750" }, + "acceleration_support_interface": { "value": "750" }, + "acceleration_topbottom": { "value": "750" }, + "acceleration_wall": { "value": "750" }, + "acceleration_wall_0": { "value": "500" }, + "jerk_layer_0": { "value": "5" }, + "jerk_prime_tower": { "value": "jerk_print" }, + "jerk_print": { "value": "7.5" }, + "jerk_support": { "value": "jerk_print" }, + "jerk_support_interface": { "value": "jerk_print" }, + "jerk_topbottom": { "value": "jerk_print" }, + "jerk_wall": { "value": "jerk_print" }, + "jerk_wall_0": { "value": "jerk_print" }, + "jerk_travel": { "value": "jerk_layer_0" }, + "jerk_travel_layer_0": { "value": "jerk_layer_0" } + } +} diff --git a/resources/definitions/atmat_galaxy_600.def.json b/resources/definitions/atmat_galaxy_600.def.json new file mode 100644 index 0000000000..c5586eb514 --- /dev/null +++ b/resources/definitions/atmat_galaxy_600.def.json @@ -0,0 +1,36 @@ +{ + "name": "Galaxy 600", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Galaxy 600" }, + "machine_width": { "default_value": 500 }, + "machine_depth": { "default_value": 500 }, + "machine_height": { "default_value": 600 }, + "acceleration_layer_0": { "value": "250" }, + "acceleration_prime_tower": { "value": "750" }, + "acceleration_print": { "value": "750" }, + "acceleration_travel": { "value": "750" }, + "acceleration_support": { "value": "750" }, + "acceleration_support_interface": { "value": "750" }, + "acceleration_topbottom": { "value": "750" }, + "acceleration_wall": { "value": "750" }, + "acceleration_wall_0": { "value": "500" }, + "jerk_layer_0": { "value": "5" }, + "jerk_prime_tower": { "value": "jerk_print" }, + "jerk_print": { "value": "7.5" }, + "jerk_support": { "value": "jerk_print" }, + "jerk_support_interface": { "value": "jerk_print" }, + "jerk_topbottom": { "value": "jerk_print" }, + "jerk_wall": { "value": "jerk_print" }, + "jerk_wall_0": { "value": "jerk_print" }, + "jerk_travel": { "value": "jerk_layer_0" }, + "jerk_travel_layer_0": { "value": "jerk_layer_0" } + } +} diff --git a/resources/definitions/atmat_signal_pro_300_v1.def.json b/resources/definitions/atmat_signal_pro_300_v1.def.json new file mode 100644 index 0000000000..27e65b92c2 --- /dev/null +++ b/resources/definitions/atmat_signal_pro_300_v1.def.json @@ -0,0 +1,36 @@ +{ + "name": "Signal Pro 300 v1", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Signal Pro 300 v1" }, + "machine_width": { "default_value": 300 }, + "machine_depth": { "default_value": 300 }, + "machine_height": { "default_value": 300 }, + "acceleration_layer_0": { "value": "250" }, + "acceleration_prime_tower": { "value": "750" }, + "acceleration_print": { "value": "750" }, + "acceleration_travel": { "value": "750" }, + "acceleration_support": { "value": "750" }, + "acceleration_support_interface": { "value": "750" }, + "acceleration_topbottom": { "value": "750" }, + "acceleration_wall": { "value": "750" }, + "acceleration_wall_0": { "value": "500" }, + "jerk_layer_0": { "value": "5" }, + "jerk_prime_tower": { "value": "jerk_print" }, + "jerk_print": { "value": "7.5" }, + "jerk_support": { "value": "jerk_print" }, + "jerk_support_interface": { "value": "jerk_print" }, + "jerk_topbottom": { "value": "jerk_print" }, + "jerk_wall": { "value": "jerk_print" }, + "jerk_wall_0": { "value": "jerk_print" }, + "jerk_travel": { "value": "jerk_layer_0" }, + "jerk_travel_layer_0": { "value": "jerk_layer_0" } + } +} diff --git a/resources/definitions/atmat_signal_pro_300_v2.def.json b/resources/definitions/atmat_signal_pro_300_v2.def.json new file mode 100644 index 0000000000..39e10ce549 --- /dev/null +++ b/resources/definitions/atmat_signal_pro_300_v2.def.json @@ -0,0 +1,18 @@ +{ + "name": "Signal Pro 300 v2", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "platform": "atmat_signal_pro_platform.stl", + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Signal Pro 300 v2" }, + "machine_width": { "default_value": 300 }, + "machine_depth": { "default_value": 300 }, + "machine_height": { "default_value": 300 } + } +} diff --git a/resources/definitions/atmat_signal_pro_400_v1.def.json b/resources/definitions/atmat_signal_pro_400_v1.def.json new file mode 100644 index 0000000000..ef0d25c0af --- /dev/null +++ b/resources/definitions/atmat_signal_pro_400_v1.def.json @@ -0,0 +1,36 @@ +{ + "name": "Signal Pro 400 v1", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Signal Pro 400 v1" }, + "machine_width": { "default_value": 300 }, + "machine_depth": { "default_value": 300 }, + "machine_height": { "default_value": 400 }, + "acceleration_layer_0": { "value": "250" }, + "acceleration_prime_tower": { "value": "750" }, + "acceleration_print": { "value": "750" }, + "acceleration_travel": { "value": "750" }, + "acceleration_support": { "value": "750" }, + "acceleration_support_interface": { "value": "750" }, + "acceleration_topbottom": { "value": "750" }, + "acceleration_wall": { "value": "750" }, + "acceleration_wall_0": { "value": "500" }, + "jerk_layer_0": { "value": "5" }, + "jerk_prime_tower": { "value": "jerk_print" }, + "jerk_print": { "value": "7.5" }, + "jerk_support": { "value": "jerk_print" }, + "jerk_support_interface": { "value": "jerk_print" }, + "jerk_topbottom": { "value": "jerk_print" }, + "jerk_wall": { "value": "jerk_print" }, + "jerk_wall_0": { "value": "jerk_print" }, + "jerk_travel": { "value": "jerk_layer_0" }, + "jerk_travel_layer_0": { "value": "jerk_layer_0" } + } +} diff --git a/resources/definitions/atmat_signal_pro_400_v2.def.json b/resources/definitions/atmat_signal_pro_400_v2.def.json new file mode 100644 index 0000000000..a147cbe2d6 --- /dev/null +++ b/resources/definitions/atmat_signal_pro_400_v2.def.json @@ -0,0 +1,18 @@ +{ + "name": "Signal Pro 400 v2", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "platform": "atmat_signal_pro_platform.stl", + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Signal Pro 400 v2" }, + "machine_width": { "default_value": 300 }, + "machine_depth": { "default_value": 300 }, + "machine_height": { "default_value": 400 } + } +} diff --git a/resources/definitions/atmat_signal_pro_500_v1.def.json b/resources/definitions/atmat_signal_pro_500_v1.def.json new file mode 100644 index 0000000000..60b5fee1bc --- /dev/null +++ b/resources/definitions/atmat_signal_pro_500_v1.def.json @@ -0,0 +1,36 @@ +{ + "name": "Signal Pro 500 v1", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Signal Pro 500 v1" }, + "machine_width": { "default_value": 300 }, + "machine_depth": { "default_value": 300 }, + "machine_height": { "default_value": 500 }, + "acceleration_layer_0": { "value": "250" }, + "acceleration_prime_tower": { "value": "750" }, + "acceleration_print": { "value": "750" }, + "acceleration_travel": { "value": "750" }, + "acceleration_support": { "value": "750" }, + "acceleration_support_interface": { "value": "750" }, + "acceleration_topbottom": { "value": "750" }, + "acceleration_wall": { "value": "750" }, + "acceleration_wall_0": { "value": "500" }, + "jerk_layer_0": { "value": "5" }, + "jerk_prime_tower": { "value": "jerk_print" }, + "jerk_print": { "value": "7.5" }, + "jerk_support": { "value": "jerk_print" }, + "jerk_support_interface": { "value": "jerk_print" }, + "jerk_topbottom": { "value": "jerk_print" }, + "jerk_wall": { "value": "jerk_print" }, + "jerk_wall_0": { "value": "jerk_print" }, + "jerk_travel": { "value": "jerk_layer_0" }, + "jerk_travel_layer_0": { "value": "jerk_layer_0" } + } +} diff --git a/resources/definitions/atmat_signal_pro_500_v2.def.json b/resources/definitions/atmat_signal_pro_500_v2.def.json new file mode 100644 index 0000000000..0ffe012155 --- /dev/null +++ b/resources/definitions/atmat_signal_pro_500_v2.def.json @@ -0,0 +1,18 @@ +{ + "name": "Signal Pro 500 v2", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "platform": "atmat_signal_pro_platform.stl", + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Signal Pro 500 v2" }, + "machine_width": { "default_value": 300 }, + "machine_depth": { "default_value": 300 }, + "machine_height": { "default_value": 500 } + } +} diff --git a/resources/definitions/atmat_signal_pro_base.def.json b/resources/definitions/atmat_signal_pro_base.def.json new file mode 100644 index 0000000000..774c179a34 --- /dev/null +++ b/resources/definitions/atmat_signal_pro_base.def.json @@ -0,0 +1,312 @@ +{ + "name": "Signal Pro Base", + "version": 2, + "inherits": "fdmprinter", + "metadata": + { + "visible": false, + "author": "ATMAT", + "manufacturer": "ATMAT sp. z o.o.", + "file_formats": "text/x-gcode", + "preferred_quality_type": "fast", + "has_machine_quality": true, + "has_materials": true, + "has_variants": true, + "variants_name": "Nozzle", + "preferred_variant_name": "V6 0.40mm", + "machine_extruder_trains": + { + "0": "atmat_signal_pro_extruder_left", + "1": "atmat_signal_pro_extruder_right" + }, + "preferred_material": "generic_pla_175", + "supports_usb_connection": false, + "supports_network_connection": false, + "exclude_materials": [ + "ultimaker_abs_black", + "ultimaker_abs_blue", + "ultimaker_abs_green", + "ultimaker_abs_grey", + "ultimaker_abs_orange", + "ultimaker_abs_pearl-gold", + "ultimaker_abs_red", + "ultimaker_abs_silver-metallic", + "ultimaker_abs_white", + "ultimaker_abs_yellow", + "ultimaker_bam", + "ultimaker_cpe_black", + "ultimaker_cpe_blue", + "ultimaker_cpe_dark-grey", + "ultimaker_cpe_green", + "ultimaker_cpe_light-grey", + "ultimaker_cpe_plus_black", + "ultimaker_cpe_plus_transparent", + "ultimaker_cpe_plus_white", + "ultimaker_cpe_red", + "ultimaker_cpe_transparent", + "ultimaker_cpe_white", + "ultimaker_cpe_yellow", + "ultimaker_nylon_black", + "ultimaker_nylon_transparent", + "ultimaker_pc_black", + "ultimaker_pc_transparent", + "ultimaker_pc_white", + "ultimaker_pla_black", + "ultimaker_pla_blue", + "ultimaker_pla_green", + "ultimaker_pla_magenta", + "ultimaker_pla_orange", + "ultimaker_pla_pearl-white", + "ultimaker_pla_red", + "ultimaker_pla_silver-metallic", + "ultimaker_pla_transparent", + "ultimaker_pla_white", + "ultimaker_pla_yellow", + "ultimaker_pp_transparent", + "ultimaker_pva", + "ultimaker_tough_pla_black", + "ultimaker_tough_pla_green", + "ultimaker_tough_pla_red", + "ultimaker_tough_pla_white", + "ultimaker_tpu_black", + "ultimaker_tpu_blue", + "ultimaker_tpu_red", + "ultimaker_tpu_white", + "chromatik_pla", + "dsm_arnitel2045_175", + "dsm_novamid1070_175", + "emotiontech_abs", + "emotiontech_petg", + "emotiontech_pla", + "emotiontech_pva-m", + "emotiontech_pva-oks", + "emotiontech_pva-s", + "emotiontech_tpu98a", + "emotiontech_asax", + "emotiontech_hips", + "fiberlogy_hd_pla", + "fabtotum_abs", + "fabtotum_nylon", + "fabtotum_pla", + "fabtotum_tpu", + "filo3d_pla", + "filo3d_pla_green", + "filo3d_pla_red", + "generic_abs", + "generic_bam", + "generic_cffcpe", + "generic_cffpa", + "generic_cpe", + "generic_cpe_plus", + "generic_gffcpe", + "generic_gffpa", + "generic_hips", + "generic_nylon", + "generic_pc", + "generic_petg", + "generic_pla", + "generic_pp", + "generic_pva", + "generic_tough_pla", + "generic_tpu", + "generic_cpe_175", + "imade3d_petg_175", + "imade3d_pla_175", + "innofill_innoflex60_175", + "leapfrog_abs_natural", + "leapfrog_epla_natural", + "leapfrog_pva_natural", + "octofiber_pla", + "polyflex_pla", + "polymax_pla", + "polyplus_pla", + "polywood_pla", + "structur3d_dap100silicone", + "tizyx_abs", + "tizyx_flex", + "tizyx_petg", + "tizyx_pla", + "tizyx_pla_bois", + "tizyx_pva", + "ultimaker_abs_black", + "ultimaker_abs_blue", + "ultimaker_abs_green", + "ultimaker_abs_grey", + "ultimaker_abs_orange", + "ultimaker_abs_pearl-gold", + "ultimaker_abs_red", + "ultimaker_abs_silver-metallic", + "ultimaker_abs_white", + "ultimaker_abs_yellow", + "ultimaker_bam", + "ultimaker_cpe_black", + "ultimaker_cpe_blue", + "ultimaker_cpe_dark-grey", + "ultimaker_cpe_green", + "ultimaker_cpe_light-grey", + "ultimaker_cpe_plus_black", + "ultimaker_cpe_plus_transparent", + "ultimaker_cpe_plus_white", + "ultimaker_cpe_red", + "ultimaker_cpe_transparent", + "ultimaker_cpe_white", + "ultimaker_cpe_yellow", + "ultimaker_nylon_black", + "ultimaker_nylon_transparent", + "ultimaker_pc_black", + "ultimaker_pc_transparent", + "ultimaker_pc_white", + "ultimaker_pla_black", + "ultimaker_pla_blue", + "ultimaker_pla_green", + "ultimaker_pla_magenta", + "ultimaker_pla_orange", + "ultimaker_pla_pearl-white", + "ultimaker_pla_red", + "ultimaker_pla_silver-metallic", + "ultimaker_pla_transparent", + "ultimaker_pla_white", + "ultimaker_pla_yellow", + "ultimaker_pp_transparent", + "ultimaker_pva", + "ultimaker_tough_pla_black", + "ultimaker_tough_pla_green", + "ultimaker_tough_pla_red", + "ultimaker_tough_pla_white", + "ultimaker_tpu_black", + "ultimaker_tpu_blue", + "ultimaker_tpu_red", + "ultimaker_tpu_white", + "verbatim_bvoh_175", + "Vertex_Delta_ABS", + "Vertex_Delta_PET", + "Vertex_Delta_PLA", + "Vertex_Delta_PLA_Glitter", + "Vertex_Delta_PLA_Mat", + "Vertex_Delta_PLA_Satin", + "Vertex_Delta_PLA_Wood", + "Vertex_Delta_TPU", + "zyyx_pro_flex", + "zyyx_pro_pla" + + ] + }, + "overrides": + { + "machine_name": { "default_value": "Signal Pro" }, + "machine_width": { "default_value": 300 }, + "machine_depth": { "default_value": 300 }, + "machine_height": { "default_value": 300 }, + "gantry_height": { "value": 30 }, + "machine_extruder_count": { "default_value": 2 }, + "machine_heated_bed": { "default_value": true }, + "machine_heated_build_volume": { "default_value": true }, + "build_volume_temperature": { "maximum_value_warning": 45 }, + "material_print_temperature": { "maximum_value_warning": 295 }, + "material_bed_temperature": { "maximum_value_warning": 140 }, + "machine_max_acceleration_x": { "default_value": 1500 }, + "machine_max_acceleration_y": { "default_value": 1500 }, + "machine_max_acceleration_z": { "default_value": 250 }, + "machine_acceleration": { "default_value": 1500 }, + "machine_max_jerk_xy": { "default_value": 15 }, + "machine_max_jerk_z": { "default_value": 1 }, + "machine_max_jerk_e": { "default_value": 15 }, + "machine_head_with_fans_polygon": { "default_value": [ [-35, 65], [-35, -50], [35, -50], [35, 65] ] }, + "machine_max_feedrate_z": { "default_value": 10 }, + "machine_max_feedrate_e": { "default_value": 120 }, + "machine_gcode_flavor": { "default_value": "Marlin" }, + "machine_start_gcode": { "default_value": "G21 ; set units to millimeters\nG90 ; use absolute positioning\nM82 ; absolute extrusion mode\nG28 ; home all without mesh bed level\nM420 S1\nG92 E0.0 ; reset extruder distance position\nG1 Z0.25\nG1 X60.0 E9.0 F1000.0 ; intro line\nG1 X100.0 E21.5 F1000.0 ; intro line\nG92 E0.0 ; reset extruder distance position" }, + "machine_end_gcode": { "default_value": "M104 T0 S0 ;extruder heater off\nM104 T1 S0 ;extruder heater off\nM140 S0 ;heated bed heater off\nG91\nG1 Z1 F420 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+1 E-1 F300 ;move Z up a bit and retract filament even more\nG90 ;absolute positioning\nG1 X0 Y300 F6000 ;move the head out of the way\nM84 ;steppers off" }, + "adhesion_type": { "default_value": "skirt" }, + "skirt_brim_minimal_length": { "value": "550" }, + "retraction_amount": { "value": "1" }, + "retraction_speed": { "value": "50", "maximum_value_warning": "130" }, + "retraction_retract_speed": { "value": "retraction_speed", "maximum_value_warning": "130" }, + "retraction_prime_speed": { "value": "math.ceil(retraction_speed * 0.4)", "maximum_value_warning": "130" }, + "retraction_hop_enabled": { "value": "True" }, + "retraction_hop": { "value": "0.5" }, + "retraction_combing": { "default_value": "noskin" }, + "retraction_combing_max_distance": { "value": "10" }, + "travel_avoid_other_parts": { "value": "True" }, + "travel_avoid_supports": { "value": "True" }, + "speed_travel": { "maximum_value": "150", "value": "150", "maximum_value_warning": "151" }, + "speed_travel_layer_0": { "value": "math.ceil(speed_travel * 0.4)" }, + "speed_layer_0": { "value": "math.ceil(speed_print * 0.25)" }, + "speed_wall": { "value": "math.ceil(speed_print * 0.50)" }, + "speed_wall_0": { "value": "math.ceil(speed_print * 0.50)" }, + "speed_wall_x": { "value": "math.ceil(speed_print * 0.75)" }, + "speed_topbottom": { "value": "math.ceil(speed_print * 0.45)" }, + "speed_roofing": { "value": "math.ceil(speed_print * 0.45)" }, + "speed_slowdown_layers": { "value": "2" }, + "roofing_layer_count": { "value": "1" }, + "optimize_wall_printing_order": { "value": "True" }, + "infill_enable_travel_optimization": { "value": "True" }, + "minimum_polygon_circumference": { "value": "0.2" }, + "wall_overhang_angle": { "value": "75" }, + "wall_overhang_speed_factor": { "value": "50" }, + "bridge_settings_enabled": { "value": "True" }, + "bridge_wall_coast": { "value": "10" }, + "bridge_fan_speed": { "value": "100" }, + "bridge_fan_speed_2": { "resolve": "max(cool_fan_speed, 50)" }, + "bridge_fan_speed_3": { "resolve": "max(cool_fan_speed, 20)" }, + "alternate_extra_perimeter": { "value": "True" }, + "cool_min_layer_time_fan_speed_max": { "value": "20" }, + "cool_min_layer_time": { "value": "15" }, + "cool_fan_speed_min": { "value": "cool_fan_speed" }, + "cool_fan_full_at_height": { "value": "resolveOrValue('layer_height_0') + resolveOrValue('layer_height') * max(1, cool_fan_full_layer - 1)" }, + "cool_fan_full_layer": { "value": "4" }, + "layer_height_0": { "resolve": "max(0.2, min(extruderValues('layer_height')))" }, + "line_width": { "value": "machine_nozzle_size * 1.125" }, + "wall_line_width": { "value": "machine_nozzle_size" }, + "fill_perimeter_gaps": { "default_value": "everywhere" }, + "fill_outline_gaps": { "value": "True" }, + "meshfix_maximum_resolution": { "value": "0.01" }, + "meshfix_maximum_deviation": { "value": "layer_height / 2" }, + "infill_before_walls": { "value": "False" }, + "zig_zaggify_infill": { "value": "True" }, + "min_infill_area": { "value": "5.0" }, + "acceleration_enabled": { "value": "True" }, + "acceleration_layer_0": { "value": "250" }, + "acceleration_prime_tower": { "value": "1000" }, + "acceleration_print": { "value": "1000" }, + "acceleration_travel": { "value": "1000" }, + "acceleration_support": { "value": "1000" }, + "acceleration_support_interface": { "value": "750" }, + "acceleration_topbottom": { "value": "750" }, + "acceleration_wall": { "value": "750" }, + "acceleration_wall_0": { "value": "500" }, + "jerk_enabled": { "value": "True" }, + "jerk_layer_0": { "value": "5" }, + "jerk_prime_tower": { "value": "jerk_print" }, + "jerk_print": { "value": "10" }, + "jerk_support": { "value": "jerk_print" }, + "jerk_support_interface": { "value": "jerk_print" }, + "jerk_topbottom": { "value": "jerk_print" }, + "jerk_wall": { "value": "jerk_print" }, + "jerk_wall_0": { "value": "jerk_print" }, + "jerk_travel": { "value": "jerk_layer_0" }, + "jerk_travel_layer_0": { "value": "jerk_layer_0" }, + "prime_tower_position_x": { "value": "270" }, + "prime_tower_position_y": { "value": "270" }, + "extruder_prime_pos_abs": { "value": "True" }, + "switch_extruder_prime_speed": { "value": "15" }, + "switch_extruder_retraction_amount": { "value": "2" }, + "support_xy_distance": { "value": "wall_line_width_0 * 2.5" }, + "support_xy_distance_overhang": { "value": "wall_line_width_0" }, + "support_angle": { "value": "60" }, + "support_bottom_distance": { "value": "support_z_distance / 2" }, + "support_pattern": { "default_value": "zigzag" }, + "support_top_distance": { "value": "support_z_distance" }, + "support_use_towers": { "value": "True" }, + "support_z_distance": { "value": "layer_height" }, + "support_interface_enable": { "value": "True" }, + "support_interface_height": { "value": "1" }, + "support_interface_skip_height": { "value": "layer_height" }, + "support_bottom_enable": { "value": "False" }, + "support_join_distance": { "value": "1" }, + "support_offset": { "value": "1.5" }, + "support_infill_rate": { "value": "20" }, + "support_brim_enable": { "value": "True" }, + "prime_tower_enable": { "value": "True" } + } +} \ No newline at end of file diff --git a/resources/definitions/atmat_signal_xl.def.json b/resources/definitions/atmat_signal_xl.def.json new file mode 100644 index 0000000000..855ac89bb5 --- /dev/null +++ b/resources/definitions/atmat_signal_xl.def.json @@ -0,0 +1,40 @@ +{ + "name": "Signal XL", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Signal XL" }, + "machine_width": { "default_value": 310 }, + "machine_depth": { "default_value": 320 }, + "machine_height": { "default_value": 260 }, + "acceleration_layer_0": { "value": "250" }, + "acceleration_prime_tower": { "value": "750" }, + "acceleration_print": { "value": "750" }, + "acceleration_travel": { "value": "750" }, + "acceleration_support": { "value": "750" }, + "acceleration_support_interface": { "value": "750" }, + "acceleration_topbottom": { "value": "750" }, + "acceleration_wall": { "value": "750" }, + "acceleration_wall_0": { "value": "500" }, + "jerk_layer_0": { "value": "5" }, + "jerk_prime_tower": { "value": "jerk_print" }, + "jerk_print": { "value": "7.5" }, + "jerk_support": { "value": "jerk_print" }, + "jerk_support_interface": { "value": "jerk_print" }, + "jerk_topbottom": { "value": "jerk_print" }, + "jerk_wall": { "value": "jerk_print" }, + "jerk_wall_0": { "value": "jerk_print" }, + "jerk_travel": { "value": "jerk_layer_0" }, + "jerk_travel_layer_0": { "value": "jerk_layer_0" }, + "machine_extruder_count": { "default_value": 1 }, + "machine_heated_build_volume": { "default_value": false }, + "machine_gcode_flavor": { "default_value": "Repetier" } + + } +} diff --git a/resources/definitions/atmat_signal_xxl.def.json b/resources/definitions/atmat_signal_xxl.def.json new file mode 100644 index 0000000000..25e2f4c102 --- /dev/null +++ b/resources/definitions/atmat_signal_xxl.def.json @@ -0,0 +1,40 @@ +{ + "name": "Signal XXL", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Signal XXL" }, + "machine_width": { "default_value": 310 }, + "machine_depth": { "default_value": 320 }, + "machine_height": { "default_value": 385 }, + "acceleration_layer_0": { "value": "250" }, + "acceleration_prime_tower": { "value": "750" }, + "acceleration_print": { "value": "750" }, + "acceleration_travel": { "value": "750" }, + "acceleration_support": { "value": "750" }, + "acceleration_support_interface": { "value": "750" }, + "acceleration_topbottom": { "value": "750" }, + "acceleration_wall": { "value": "750" }, + "acceleration_wall_0": { "value": "500" }, + "jerk_layer_0": { "value": "5" }, + "jerk_prime_tower": { "value": "jerk_print" }, + "jerk_print": { "value": "7.5" }, + "jerk_support": { "value": "jerk_print" }, + "jerk_support_interface": { "value": "jerk_print" }, + "jerk_topbottom": { "value": "jerk_print" }, + "jerk_wall": { "value": "jerk_print" }, + "jerk_wall_0": { "value": "jerk_print" }, + "jerk_travel": { "value": "jerk_layer_0" }, + "jerk_travel_layer_0": { "value": "jerk_layer_0" }, + "machine_extruder_count": { "default_value": 1 }, + "machine_heated_build_volume": { "default_value": false }, + "machine_gcode_flavor": { "default_value": "Repetier" } + + } +} diff --git a/resources/definitions/atmat_signal_xxxl.def.json b/resources/definitions/atmat_signal_xxxl.def.json new file mode 100644 index 0000000000..502ce272f0 --- /dev/null +++ b/resources/definitions/atmat_signal_xxxl.def.json @@ -0,0 +1,40 @@ +{ + "name": "Signal XXXL", + "version": 2, + "inherits": "atmat_signal_pro_base", + "metadata": + { + "visible": true, + "quality_definition": "atmat_signal_pro_base" + }, + "overrides": + { + "machine_name": { "default_value": "Signal XXXL" }, + "machine_width": { "default_value": 310 }, + "machine_depth": { "default_value": 320 }, + "machine_height": { "default_value": 500 }, + "acceleration_layer_0": { "value": "250" }, + "acceleration_prime_tower": { "value": "750" }, + "acceleration_print": { "value": "750" }, + "acceleration_travel": { "value": "750" }, + "acceleration_support": { "value": "750" }, + "acceleration_support_interface": { "value": "750" }, + "acceleration_topbottom": { "value": "750" }, + "acceleration_wall": { "value": "750" }, + "acceleration_wall_0": { "value": "500" }, + "jerk_layer_0": { "value": "5" }, + "jerk_prime_tower": { "value": "jerk_print" }, + "jerk_print": { "value": "7.5" }, + "jerk_support": { "value": "jerk_print" }, + "jerk_support_interface": { "value": "jerk_print" }, + "jerk_topbottom": { "value": "jerk_print" }, + "jerk_wall": { "value": "jerk_print" }, + "jerk_wall_0": { "value": "jerk_print" }, + "jerk_travel": { "value": "jerk_layer_0" }, + "jerk_travel_layer_0": { "value": "jerk_layer_0" }, + "machine_extruder_count": { "default_value": 1 }, + "machine_heated_build_volume": { "default_value": false }, + "machine_gcode_flavor": { "default_value": "Repetier" } + + } +} diff --git a/resources/definitions/dagoma_delta.def.json b/resources/definitions/dagoma_delta.def.json new file mode 100644 index 0000000000..6ff837e757 --- /dev/null +++ b/resources/definitions/dagoma_delta.def.json @@ -0,0 +1,68 @@ +{ + "name": "Dagoma Delta", + "version": 2, + "inherits": "fdmprinter", + "metadata": { + "visible": false, + "author": "Dagoma", + "manufacturer": "Dagoma" + }, + "overrides": { + "machine_width": { + "default_value": 195.55 + }, + "machine_height": { + "default_value": 205 + }, + "machine_depth": { + "default_value": 195.55 + }, + "machine_center_is_zero": { + "default_value": true + }, + "machine_head_with_fans_polygon": { + "default_value": [ + [-36, -42], + [-36, 42], + [36, 42], + [36, -42] + ] + }, + "gantry_height": { + "value": "0" + }, + "machine_shape": { + "default_value": "elliptic" + }, + "machine_start_gcode": { + "default_value": ";Gcode by Cura\nG90\nG28\nM107\nM109 R100\nG29\nM109 S{material_print_temperature_layer_0} U-55 X55 V-85 Y-85 W0.26 Z0.26\nM82\nG92 E0\nG1 F200 E6\nG92 E0\nG1 F200 E-3.5\nG0 Z0.15\nG0 X10\nG0 Z3\nG1 F6000\n" + }, + "machine_end_gcode": { + "default_value": "\nM104 S0\nM106 S255\nM140 S0\nG91\nG1 E-1 F300\nG1 Z+3 E-2 F9000\nG90\nG28\n" + }, + "default_material_print_temperature": { + "default_value": 205 + }, + "speed_print": { + "default_value": 40 + }, + "retraction_amount": { + "default_value": 3.8 + }, + "retraction_speed": { + "default_value": 60 + }, + "adhesion_type": { + "default_value": "skirt" + }, + "skirt_line_count": { + "default_value": 2 + }, + "layer_height_0": { + "default_value": 0.26 + }, + "top_bottom_thickness": { + "default_value": 1 + } + } +} diff --git a/resources/definitions/dagoma_disco.def.json b/resources/definitions/dagoma_disco.def.json new file mode 100644 index 0000000000..a62948c9a7 --- /dev/null +++ b/resources/definitions/dagoma_disco.def.json @@ -0,0 +1,62 @@ +{ + "name": "Dagoma Disco", + "version": 2, + "inherits": "fdmprinter", + "metadata": { + "visible": false, + "author": "Dagoma", + "manufacturer": "Dagoma" + }, + "overrides": { + "machine_width": { + "default_value": 205 + }, + "machine_height": { + "default_value": 205 + }, + "machine_depth": { + "default_value": 205 + }, + "machine_center_is_zero": { + "default_value": false + }, + "machine_head_with_fans_polygon": { + "default_value": [ + [-17, -70], + [-17, 40], + [17, 40], + [17, -70] + ] + }, + "gantry_height": { + "value": "10" + }, + "default_material_print_temperature": { + "default_value": 205 + }, + "material_standby_temperature": { + "default_value": 90 + }, + "speed_print": { + "default_value": 60 + }, + "retraction_amount": { + "default_value": 3.5 + }, + "retraction_speed": { + "default_value": 50 + }, + "adhesion_type": { + "default_value": "skirt" + }, + "skirt_line_count": { + "default_value": 2 + }, + "layer_height_0": { + "default_value": 0.26 + }, + "top_bottom_thickness": { + "default_value": 1 + } + } +} diff --git a/resources/definitions/dagoma_discoeasy200.def.json b/resources/definitions/dagoma_discoeasy200.def.json index 1032a249f8..16ce5585e4 100644 --- a/resources/definitions/dagoma_discoeasy200.def.json +++ b/resources/definitions/dagoma_discoeasy200.def.json @@ -1,82 +1,34 @@ { "name": "Dagoma DiscoEasy200", "version": 2, - "inherits": "fdmprinter", + "inherits": "dagoma_disco", "metadata": { "visible": true, "author": "Dagoma", "manufacturer": "Dagoma", "file_formats": "text/x-gcode", "platform": "dagoma_discoeasy200.3mf", - "platform_offset": [0, -57.3, -11], + "platform_offset": [0, -57, -39], "has_machine_quality": true, "has_materials": true, "preferred_material": "chromatik_pla", "machine_extruder_trains": { - "0": "dagoma_discoeasy200_extruder_0", - "1": "dagoma_discoeasy200_extruder_1" + "0": "dagoma_discoeasy200_extruder" } }, "overrides": { + "machine_name": { + "default_value": "Dagoma DiscoEasy200" + }, "machine_extruder_count": { - "default_value": 2 - }, - "machine_extruders_share_heater": { - "default_value": true - }, - "machine_width": { - "default_value": 205 - }, - "machine_height": { - "default_value": 205 - }, - "machine_depth": { - "default_value": 205 - }, - "machine_center_is_zero": { - "default_value": false - }, - "machine_head_with_fans_polygon": { - "default_value": [ - [-17, -70], - [-17, 40], - [17, 40], - [17, -70] - ] - }, - "gantry_height": { - "value": "10" + "default_value": 1 }, "machine_start_gcode": { - "default_value": ";Gcode by Cura\nG90\nM106 S255\nG28 X Y\nG1 X50\nM109 R90\nG28\nM104 S{material_print_temperature_layer_0}\nG29\nM107\nG1 X100 Y20 F3000\nG1 Z0.5\nM109 S{material_print_temperature_layer_0}\nM82\nG92 E0\nG1 F200 E10\nG92 E0\nG1 Z3\nG1 F6000\n" + "default_value": ";Begin Start Gcode for Dagoma DiscoEasy 200\n;Sliced: {date} {time}\n;Initial extruder: {initial_extruder_nr}\n\nG90 ;Absolute positioning\nM106 S255 ;Fan on full\nG28 X Y ;Home stop X Y\nG1 X100 ;Centre back during cooldown in case of oozing\nM109 R{material_standby_temperature} ;Cooldown in case too hot\nG28 ;Centre\nG29 ;Auto-level\nM104 S{material_print_temperature_layer_0} ;Pre-heat\nM107 ;Fan off\nG0 X100 Y5 Z0.5 ;Front centre for degunk\nM109 S{material_print_temperature_layer_0} ;Wait for initial temp\nM83 ;E Relative\nG1 E10 F200 ;Degunk\nG1 E-3 F5000 ;Retract\nG0 Z3 ;Withdraw\nM82 ;E absolute\nG92 E0 ;E reset\nG1 F6000 ;Set feedrate\n\n;Finish Start Gcode for Dagoma DiscoEasy 200\n" }, "machine_end_gcode": { - "default_value": "\nM104 S0\nM106 S255\nM140 S0\nG91\nG1 E-1 F300\nG1 Z+3 F3000\nG90\nG28 X Y\nM107\nM84\n" - }, - "default_material_print_temperature": { - "default_value": 205 - }, - "speed_print": { - "default_value": 60 - }, - "retraction_amount": { - "default_value": 3.5 - }, - "retraction_speed": { - "default_value": 50 - }, - "adhesion_type": { - "default_value": "skirt" - }, - "skirt_line_count": { - "default_value": 2 - }, - "layer_height_0": { - "default_value": 0.26 - }, - "top_bottom_thickness": { - "default_value": 1 + "default_value": ";Begin End Gcode for Dagoma DiscoEasy 200\n\nM106 S255 ;Fan on full\nM104 S0 ;Cool hotend\nM140 S0 ;Cool heated bed\nG91 ;Relative positioning\nG1 E-3 F5000 ;Retract filament to stop oozing\nG0 Z+3 ;Withdraw\nG90 ;Absolute positioning\nG28 X Y ;Home\nM109 R{material_standby_temperature} ;Wait until head has cooled to standby temp\nM107 ;Fan off\nM18 ;Stepper motors off\n\n;Finish End Gcode for Dagoma DiscoEasy 200\n" } } } diff --git a/resources/definitions/dagoma_discoeasy200_bicolor.def.json b/resources/definitions/dagoma_discoeasy200_bicolor.def.json new file mode 100644 index 0000000000..4786c03fc2 --- /dev/null +++ b/resources/definitions/dagoma_discoeasy200_bicolor.def.json @@ -0,0 +1,38 @@ +{ + "name": "Dagoma DiscoEasy200 Bicolor", + "version": 2, + "inherits": "dagoma_disco", + "metadata": { + "visible": true, + "author": "Dagoma", + "manufacturer": "Dagoma", + "file_formats": "text/x-gcode", + "platform": "dagoma_discoeasy200_bicolor.3mf", + "platform_offset": [0, -57.3, -11], + "has_machine_quality": true, + "has_materials": true, + "preferred_material": "chromatik_pla", + "machine_extruder_trains": + { + "0": "dagoma_discoeasy200_extruder_0", + "1": "dagoma_discoeasy200_extruder_1" + } + }, + "overrides": { + "machine_name": { + "default_value": "Dagoma DiscoEasy200 Bicolor" + }, + "machine_extruder_count": { + "default_value": 2 + }, + "machine_extruders_share_heater": { + "default_value": true + }, + "machine_start_gcode": { + "default_value": ";Begin Start Gcode for Dagoma DiscoEasy 200 Bicolor\n;Sliced: {date} {time}\n;Initial extruder: {initial_extruder_nr}\n\nG90 ;Absolute positioning\nM106 S255 ;Fan on full\nG28 X Y ;Home stop X Y\nG1 X100 ;Centre back during cooldown in case of oozing\nM109 R{material_standby_temperature} ;Cooldown in case too hot\nG28 ;Centre\nG29 ;Auto-level\nM104 S{material_print_temperature_layer_0} ;Pre-heat\nM107 ;Fan off\nG0 X100 Y5 Z0.5 ;Front centre for degunk\nM109 S{material_print_temperature_layer_0} ;Wait for initial temp\n;M83 ;E Relative\n;G1 E60 F3000 ;Reverse multi-extruder retract\n;G1 E10 F200 ;Degunk\n;G1 E-3 F5000 ;Retract\nG0 Z3 ;Withdraw\n;M82 ;E absolute\n;G92 E0 ;E reset\n;G1 F6000 ;Set feedrate\n\n;Finish Start Gcode for Dagoma DiscoEasy 200 Bicolor\n" + }, + "machine_end_gcode": { + "default_value": ";Begin End Gcode for Dagoma DiscoEasy 200 Bicolor\n\nM106 S255 ;Fan on full\nM104 S0 ;Cool hotend\nM140 S0 ;Cool heated bed\nG91 ;Relative positioning\nG1 E-3 F5000 ;Retract filament to stop oozing\nG1 E-60 F5000 ;Retract filament multi-extruder\nG0 Z+3 ;Withdraw\nG90 ; Absolute positioning\nG28 X Y ;Home\nM109 R{material_standby_temperature} ;Wait until head has cooled to standby temp\nM107 ;Fan off\nM18 ;Stepper motors off\n\n;Finish End Gcode for Dagoma DiscoEasy 200 Bicolor\n" + } + } +} diff --git a/resources/definitions/dagoma_discoultimate.def.json b/resources/definitions/dagoma_discoultimate.def.json index 13ab591212..ec41318e86 100644 --- a/resources/definitions/dagoma_discoultimate.def.json +++ b/resources/definitions/dagoma_discoultimate.def.json @@ -1,82 +1,34 @@ { "name": "Dagoma DiscoUltimate", "version": 2, - "inherits": "fdmprinter", + "inherits": "dagoma_disco", "metadata": { "visible": true, "author": "Dagoma", "manufacturer": "Dagoma", "file_formats": "text/x-gcode", "platform": "dagoma_discoultimate.3mf", - "platform_offset": [0, -58.5, -11], + "platform_offset": [0, -58.5, -39.5], "has_machine_quality": true, "has_materials": true, "preferred_material": "chromatik_pla", "machine_extruder_trains": { - "0": "dagoma_discoultimate_extruder_0", - "1": "dagoma_discoultimate_extruder_1" + "0": "dagoma_discoultimate_extruder" } }, "overrides": { + "machine_name": { + "default_value": "Dagoma DiscoUltimate" + }, "machine_extruder_count": { - "default_value": 2 - }, - "machine_extruders_share_heater": { - "default_value": true - }, - "machine_width": { - "default_value": 205 - }, - "machine_height": { - "default_value": 205 - }, - "machine_depth": { - "default_value": 205 - }, - "machine_center_is_zero": { - "default_value": false - }, - "machine_head_with_fans_polygon": { - "default_value": [ - [-17, -70], - [-17, 40], - [17, 40], - [17, -70] - ] - }, - "gantry_height": { - "value": "10" + "default_value": 1 }, "machine_start_gcode": { - "default_value": ";Gcode by Cura\nG90\nM106 S255\nG28 X Y\nG1 X50\nM109 R90\nG28\nM104 S{material_print_temperature_layer_0}\nG29\nM107\nG1 X100 Y20 F3000\nG1 Z0.5\nM109 S{material_print_temperature_layer_0}\nM82\nG92 E0\nG1 F200 E10\nG92 E0\nG1 Z3\nG1 F6000\n" + "default_value": ";Begin Start Gcode for Dagoma DiscoUltimate\n;Sliced: {date} {time}\n;Initial extruder: {initial_extruder_nr}\n\nG90 ;Absolute positioning\nM106 S255 ;Fan on full\nG28 X Y ;Home stop X Y\nG1 X100 ;Centre back during cooldown in case of oozing\nM109 R{material_standby_temperature} ;Cooldown in case too hot\nG28 ;Centre\nG29 ;Auto-level\nM104 S{material_print_temperature_layer_0} ;Pre-heat\nM107 ;Fan off\nG0 X100 Y5 Z0.5 ;Front centre for degunk\nM109 S{material_print_temperature_layer_0} ;Wait for initial temp\nM83 ;E Relative\nG1 E10 F200 ;Degunk\nG1 E-3 F5000 ;Retract\nG0 Z3 ;Withdraw\nM82 ;E absolute\nG92 E0 ;E reset\nG1 F6000 ;Set feedrate\n\n;Finish Start Gcode for Dagoma DiscoUltimate\n" }, "machine_end_gcode": { - "default_value": "\nM104 S0\nM106 S255\nM140 S0\nG91\nG1 E-1 F300\nG1 Z+3 F3000\nG90\nG28 X Y\nM107\nM84\n" - }, - "default_material_print_temperature": { - "default_value": 205 - }, - "speed_print": { - "default_value": 60 - }, - "retraction_amount": { - "default_value": 3.5 - }, - "retraction_speed": { - "default_value": 50 - }, - "adhesion_type": { - "default_value": "skirt" - }, - "skirt_line_count": { - "default_value": 2 - }, - "layer_height_0": { - "default_value": 0.26 - }, - "top_bottom_thickness": { - "default_value": 1 + "default_value": ";Begin End Gcode for Dagoma DiscoUltimate\n\nM106 S255 ;Fan on full\nM104 S0 ;Cool hotend\nM140 S0 ;Cool heated bed\nG91 ;Relative positioning\nG1 E-3 F5000 ;Retract filament to stop oozing\nG0 Z+3 ;Withdraw\nG90 ;Absolute positioning\nG28 X Y ;Home\nM109 R{material_standby_temperature} ;Wait until head has cooled to standby temp\nM107 ;Fan off\nM18 ;Stepper motors off\n\n;Finish End Gcode for Dagoma DiscoUltimate\n" } } } diff --git a/resources/definitions/dagoma_discoultimate_bicolor.def.json b/resources/definitions/dagoma_discoultimate_bicolor.def.json new file mode 100644 index 0000000000..3b5215c944 --- /dev/null +++ b/resources/definitions/dagoma_discoultimate_bicolor.def.json @@ -0,0 +1,38 @@ +{ + "name": "Dagoma DiscoUltimate Bicolor", + "version": 2, + "inherits": "dagoma_disco", + "metadata": { + "visible": true, + "author": "Dagoma", + "manufacturer": "Dagoma", + "file_formats": "text/x-gcode", + "platform": "dagoma_discoultimate_bicolor.3mf", + "platform_offset": [0, -58.5, -11], + "has_machine_quality": true, + "has_materials": true, + "preferred_material": "chromatik_pla", + "machine_extruder_trains": + { + "0": "dagoma_discoultimate_extruder_0", + "1": "dagoma_discoultimate_extruder_1" + } + }, + "overrides": { + "machine_name": { + "default_value": "Dagoma DiscoUltimate Bicolor" + }, + "machine_extruder_count": { + "default_value": 2 + }, + "machine_extruders_share_heater": { + "default_value": true + }, + "machine_start_gcode": { + "default_value": ";Begin Start Gcode for Dagoma DiscoUltimate Bicolor\n;Sliced: {date} {time}\n;Initial extruder: {initial_extruder_nr}\n\nG90 ;Absolute positioning\nM106 S255 ;Fan on full\nG28 X Y ;Home stop X Y\nG1 X100 ;Centre back during cooldown in case of oozing\nM109 R{material_standby_temperature} ;Cooldown in case too hot\nG28 ;Centre\nG29 ;Auto-level\nM104 S{material_print_temperature_layer_0} ;Pre-heat\nM107 ;Fan off\nG0 X100 Y5 Z0.5 ;Front centre for degunk\nM109 S{material_print_temperature_layer_0} ;Wait for initial temp\n;M83 ;E Relative\n;G1 E60 F3000 ;Reverse multi-extruder retract\n;G1 E10 F200 ;Degunk\n;G1 E-3 F5000 ;Retract\nG0 Z3 ;Withdraw\n;M82 ;E absolute\n;G92 E0 ;E reset\n;G1 F6000 ;Set feedrate\n\n;Finish Start Gcode for Dagoma DiscoUltimate Bicolor\n" + }, + "machine_end_gcode": { + "default_value": ";Begin End Gcode for Dagoma DiscoUltimate Bicolor\n\nM106 S255 ;Fan on full\nM104 S0 ;Cool hotend\nM140 S0 ;Cool heated bed\nG91 ;Relative positioning\nG1 E-3 F5000 ;Retract filament to stop oozing\nG1 E-60 F5000 ;Retract filament multi-extruder\nG0 Z+3 ;Withdraw\nG90 ; Absolute positioning\nG28 X Y ;Home\nM109 R{material_standby_temperature} ;Wait until head has cooled to standby temp\nM107 ;Fan off\nM18 ;Stepper motors off\n\n;Finish End Gcode for Dagoma DiscoUltimate Bicolor\n" + } + } +} diff --git a/resources/definitions/dagoma_magis.def.json b/resources/definitions/dagoma_magis.def.json index e7c0847e7e..e3c9c3e693 100644 --- a/resources/definitions/dagoma_magis.def.json +++ b/resources/definitions/dagoma_magis.def.json @@ -1,7 +1,7 @@ { "name": "Dagoma Magis", "version": 2, - "inherits": "fdmprinter", + "inherits": "dagoma_delta", "metadata": { "visible": true, "author": "Dagoma", @@ -14,65 +14,12 @@ "preferred_material": "chromatik_pla", "machine_extruder_trains": { - "0": "dagoma_magis_extruder_0" + "0": "dagoma_magis_extruder" } }, "overrides": { - "machine_width": { - "default_value": 195.55 - }, - "machine_height": { - "default_value": 205 - }, - "machine_depth": { - "default_value": 195.55 - }, - "machine_center_is_zero": { - "default_value": true - }, - "machine_head_with_fans_polygon": { - "default_value": [ - [-36, -42], - [-36, 42], - [36, 42], - [36, -42] - ] - }, - "gantry_height": { - "value": "0" - }, - "machine_shape": { - "default_value": "elliptic" - }, - "machine_start_gcode": { - "default_value": ";Gcode by Cura\nG90\nG28\nM107\nM109 R100\nG29\nM109 S{material_print_temperature_layer_0} U-55 X55 V-85 Y-85 W0.26 Z0.26\nM82\nG92 E0\nG1 F200 E6\nG92 E0\nG1 F200 E-3.5\nG0 Z0.15\nG0 X10\nG0 Z3\nG1 F6000\n" - }, - "machine_end_gcode": { - "default_value": "\nM104 S0\nM106 S255\nM140 S0\nG91\nG1 E-1 F300\nG1 Z+3 E-2 F9000\nG90\nG28\n" - }, - "default_material_print_temperature": { - "default_value": 205 - }, - "speed_print": { - "default_value": 40 - }, - "retraction_amount": { - "default_value": 3.8 - }, - "retraction_speed": { - "default_value": 60 - }, - "adhesion_type": { - "default_value": "skirt" - }, - "skirt_line_count": { - "default_value": 2 - }, - "layer_height_0": { - "default_value": 0.26 - }, - "top_bottom_thickness": { - "default_value": 1 + "machine_name": { + "default_value": "Dagoma Magis" } } } diff --git a/resources/definitions/dagoma_neva.def.json b/resources/definitions/dagoma_neva.def.json index f6a6ccf511..24d3f8ab8d 100644 --- a/resources/definitions/dagoma_neva.def.json +++ b/resources/definitions/dagoma_neva.def.json @@ -1,7 +1,7 @@ { - "name": "Dagoma NEVA", + "name": "Dagoma Neva", "version": 2, - "inherits": "fdmprinter", + "inherits": "dagoma_delta", "metadata": { "visible": true, "author": "Dagoma", @@ -14,65 +14,12 @@ "preferred_material": "chromatik_pla", "machine_extruder_trains": { - "0": "dagoma_neva_extruder_0" + "0": "dagoma_neva_extruder" } }, "overrides": { - "machine_width": { - "default_value": 195.55 - }, - "machine_height": { - "default_value": 205 - }, - "machine_depth": { - "default_value": 195.55 - }, - "machine_center_is_zero": { - "default_value": true - }, - "machine_head_with_fans_polygon": { - "default_value": [ - [-36, -42], - [-36, 42], - [36, 42], - [36, -42] - ] - }, - "gantry_height": { - "value": "0" - }, - "machine_shape": { - "default_value": "elliptic" - }, - "machine_start_gcode": { - "default_value": ";Gcode by Cura\nG90\nG28\nM107\nM109 R100\nG29\nM109 S{material_print_temperature_layer_0} U-55 X55 V-85 Y-85 W0.26 Z0.26\nM82\nG92 E0\nG1 F200 E6\nG92 E0\nG1 F200 E-3.5\nG0 Z0.15\nG0 X10\nG0 Z3\nG1 F6000\n" - }, - "machine_end_gcode": { - "default_value": "\nM104 S0\nM106 S255\nM140 S0\nG91\nG1 E-1 F300\nG1 Z+3 E-2 F9000\nG90\nG28\n" - }, - "default_material_print_temperature": { - "default_value": 205 - }, - "speed_print": { - "default_value": 40 - }, - "retraction_amount": { - "default_value": 3.8 - }, - "retraction_speed": { - "default_value": 60 - }, - "adhesion_type": { - "default_value": "skirt" - }, - "skirt_line_count": { - "default_value": 2 - }, - "layer_height_0": { - "default_value": 0.26 - }, - "top_bottom_thickness": { - "default_value": 1 + "machine_name": { + "default_value": "Dagoma Neva" } } } diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 9da1829d9c..babd4cca3e 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -219,6 +219,16 @@ "settable_per_extruder": false, "settable_per_meshgroup": false }, + "machine_always_write_active_tool": + { + "label": "Always Write Active Tool", + "description": "Write active tool after sending temp commands to inactive tool. Required for Dual Extruder printing with Smoothie or other firmware with modal tool commands.", + "default_value": false, + "type": "bool", + "settable_per_mesh": false, + "settable_per_extruder": false, + "settable_per_meshgroup": false + }, "machine_center_is_zero": { "label": "Is Center Origin", @@ -877,7 +887,7 @@ "maximum_value_warning": "3 * machine_nozzle_size", "default_value": 0.4, "type": "float", - "enabled": "(support_enable or support_tree_enable)", + "enabled": "(support_enable or support_tree_enable or support_meshes_present)", "value": "line_width", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, @@ -893,7 +903,7 @@ "minimum_value_warning": "0.1 + 0.4 * machine_nozzle_size", "maximum_value_warning": "2 * machine_nozzle_size", "type": "float", - "enabled": "(support_enable or support_tree_enable) and support_interface_enable", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_interface_enable", "limit_to_extruder": "support_interface_extruder_nr", "value": "line_width", "settable_per_mesh": false, @@ -910,7 +920,7 @@ "minimum_value_warning": "0.4 * machine_nozzle_size", "maximum_value_warning": "2 * machine_nozzle_size", "type": "float", - "enabled": "(support_enable or support_tree_enable) and support_roof_enable", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_roof_enable", "limit_to_extruder": "support_roof_extruder_nr", "value": "extruderValue(support_roof_extruder_nr, 'support_interface_line_width')", "settable_per_mesh": false, @@ -926,7 +936,7 @@ "minimum_value_warning": "0.4 * machine_nozzle_size", "maximum_value_warning": "2 * machine_nozzle_size", "type": "float", - "enabled": "(support_enable or support_tree_enable) and support_bottom_enable", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_bottom_enable", "limit_to_extruder": "support_bottom_extruder_nr", "value": "extruderValue(support_bottom_extruder_nr, 'support_interface_line_width')", "settable_per_mesh": false, @@ -1575,7 +1585,7 @@ "type": "float", "unit": "mm", "default_value": 0.35, - "value": "wall_line_width_0 / 2", + "value": "wall_line_width_0 / 2 + (ironing_line_spacing - skin_line_width * (1.0 + ironing_flow / 100) / 2 if ironing_pattern == 'concentric' else skin_line_width * (1.0 - ironing_flow / 100) / 2)", "minimum_value_warning": "0", "maximum_value_warning": "wall_line_width_0", "enabled": "ironing_enabled", @@ -1761,7 +1771,7 @@ "description": "A list of integer line directions to use. Elements from the list are used sequentially as the layers progress and when the end of the list is reached, it starts at the beginning again. The list items are separated by commas and the whole list is contained in square brackets. Default is an empty list which means use the traditional default angles (45 and 135 degrees for the lines and zig zag patterns and 45 degrees for all other patterns).", "type": "[int]", "default_value": "[ ]", - "enabled": "infill_pattern != 'concentric' and infill_pattern != 'cubicsubdiv' and infill_sparse_density > 0", + "enabled": "infill_pattern != 'concentric' and infill_sparse_density > 0", "limit_to_extruder": "infill_extruder_nr", "settable_per_mesh": true }, @@ -2072,7 +2082,7 @@ "description": "Skin areas narrower than this are not expanded. This avoids expanding the narrow skin areas that are created when the model surface has a slope close to the vertical.", "unit": "mm", "type": "float", - "default_value": 2.24, + "default_value": 0, "value": "top_layers * layer_height / math.tan(math.radians(max_skin_angle_for_expansion))", "minimum_value": "0", "enabled": "(top_layers > 0 or bottom_layers > 0) and (top_skin_expand_distance > 0 or bottom_skin_expand_distance > 0)", @@ -2836,7 +2846,7 @@ "maximum_value_warning": "150", "default_value": 60, "value": "speed_print", - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "settable_per_mesh": false, "limit_to_extruder": "support_extruder_nr", "settable_per_extruder": true, @@ -2853,7 +2863,7 @@ "maximum_value": "math.sqrt(machine_max_feedrate_x ** 2 + machine_max_feedrate_y ** 2)", "maximum_value_warning": "150", "value": "speed_support", - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -2868,7 +2878,7 @@ "minimum_value": "0.1", "maximum_value": "math.sqrt(machine_max_feedrate_x ** 2 + machine_max_feedrate_y ** 2)", "maximum_value_warning": "150", - "enabled": "support_interface_enable and (support_enable or support_tree_enable)", + "enabled": "support_interface_enable and (support_enable or support_tree_enable or support_meshes_present)", "limit_to_extruder": "support_interface_extruder_nr", "value": "speed_support / 1.5", "settable_per_mesh": false, @@ -2885,7 +2895,7 @@ "minimum_value": "0.1", "maximum_value": "math.sqrt(machine_max_feedrate_x ** 2 + machine_max_feedrate_y ** 2)", "maximum_value_warning": "150", - "enabled": "support_roof_enable and (support_enable or support_tree_enable)", + "enabled": "support_roof_enable and (support_enable or support_tree_enable or support_meshes_present)", "limit_to_extruder": "support_roof_extruder_nr", "value": "extruderValue(support_roof_extruder_nr, 'speed_support_interface')", "settable_per_mesh": false, @@ -2901,7 +2911,7 @@ "minimum_value": "0.1", "maximum_value": "math.sqrt(machine_max_feedrate_x ** 2 + machine_max_feedrate_y ** 2)", "maximum_value_warning": "150", - "enabled": "support_bottom_enable and (support_enable or support_tree_enable)", + "enabled": "support_bottom_enable and (support_enable or support_tree_enable or support_meshes_present)", "limit_to_extruder": "support_bottom_extruder_nr", "value": "extruderValue(support_bottom_extruder_nr, 'speed_support_interface')", "settable_per_mesh": false, @@ -3176,7 +3186,7 @@ "maximum_value_warning": "10000", "default_value": 3000, "value": "acceleration_print", - "enabled": "resolveOrValue('acceleration_enabled') and (support_enable or support_tree_enable)", + "enabled": "resolveOrValue('acceleration_enabled') and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "limit_to_extruder": "support_extruder_nr", "settable_per_extruder": true, @@ -3193,7 +3203,7 @@ "minimum_value": "0.1", "minimum_value_warning": "100", "maximum_value_warning": "10000", - "enabled": "resolveOrValue('acceleration_enabled') and (support_enable or support_tree_enable)", + "enabled": "resolveOrValue('acceleration_enabled') and (support_enable or support_tree_enable or support_meshes_present)", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -3209,7 +3219,7 @@ "minimum_value": "0.1", "minimum_value_warning": "100", "maximum_value_warning": "10000", - "enabled": "resolveOrValue('acceleration_enabled') and support_interface_enable and (support_enable or support_tree_enable)", + "enabled": "resolveOrValue('acceleration_enabled') and support_interface_enable and (support_enable or support_tree_enable or support_meshes_present)", "limit_to_extruder": "support_interface_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true, @@ -3226,7 +3236,7 @@ "minimum_value": "0.1", "minimum_value_warning": "100", "maximum_value_warning": "10000", - "enabled": "acceleration_enabled and support_roof_enable and (support_enable or support_tree_enable)", + "enabled": "acceleration_enabled and support_roof_enable and (support_enable or support_tree_enable or support_meshes_present)", "limit_to_extruder": "support_roof_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -3242,7 +3252,7 @@ "minimum_value": "0.1", "minimum_value_warning": "100", "maximum_value_warning": "10000", - "enabled": "acceleration_enabled and support_bottom_enable and (support_enable or support_tree_enable)", + "enabled": "acceleration_enabled and support_bottom_enable and (support_enable or support_tree_enable or support_meshes_present)", "limit_to_extruder": "support_bottom_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -3461,7 +3471,7 @@ "maximum_value_warning": "50", "default_value": 20, "value": "jerk_print", - "enabled": "resolveOrValue('jerk_enabled') and (support_enable or support_tree_enable)", + "enabled": "resolveOrValue('jerk_enabled') and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": true, "limit_to_extruder": "support_extruder_nr", @@ -3477,7 +3487,7 @@ "value": "jerk_support", "minimum_value": "0", "maximum_value_warning": "50", - "enabled": "resolveOrValue('jerk_enabled') and (support_enable or support_tree_enable)", + "enabled": "resolveOrValue('jerk_enabled') and (support_enable or support_tree_enable or support_meshes_present)", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -3492,7 +3502,7 @@ "value": "jerk_support", "minimum_value": "0", "maximum_value_warning": "50", - "enabled": "resolveOrValue('jerk_enabled') and support_interface_enable and (support_enable or support_tree_enable)", + "enabled": "resolveOrValue('jerk_enabled') and support_interface_enable and (support_enable or support_tree_enable or support_meshes_present)", "limit_to_extruder": "support_interface_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true, @@ -3508,7 +3518,7 @@ "value": "extruderValue(support_roof_extruder_nr, 'jerk_support_interface')", "minimum_value": "0", "maximum_value_warning": "50", - "enabled": "resolveOrValue('jerk_enabled') and support_roof_enable and (support_enable or support_tree_enable)", + "enabled": "resolveOrValue('jerk_enabled') and support_roof_enable and (support_enable or support_tree_enable or support_meshes_present)", "limit_to_extruder": "support_roof_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -3523,7 +3533,7 @@ "value": "extruderValue(support_roof_extruder_nr, 'jerk_support_interface')", "minimum_value": "0", "maximum_value_warning": "50", - "enabled": "resolveOrValue('jerk_enabled') and support_bottom_enable and (support_enable or support_tree_enable)", + "enabled": "resolveOrValue('jerk_enabled') and support_bottom_enable and (support_enable or support_tree_enable or support_meshes_present)", "limit_to_extruder": "support_bottom_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -3767,7 +3777,7 @@ "description": "Omit retraction when moving from support to support in a straight line. Enabling this setting saves print time, but can lead to excessive stringing within the support structure.", "type": "bool", "default_value": true, - "enabled": "retraction_enable and (support_enable or support_tree_enable)", + "enabled": "retraction_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -4097,7 +4107,7 @@ "type": "extruder", "default_value": "0", "value": "int(defaultExtruderPosition())", - "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false, "children": { @@ -4108,7 +4118,7 @@ "type": "extruder", "default_value": "0", "value": "support_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false }, @@ -4119,7 +4129,7 @@ "type": "extruder", "default_value": "0", "value": "support_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false }, @@ -4130,7 +4140,7 @@ "type": "extruder", "default_value": "0", "value": "support_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false, "children": @@ -4142,7 +4152,7 @@ "type": "extruder", "default_value": "0", "value": "support_interface_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false }, @@ -4153,7 +4163,7 @@ "type": "extruder", "default_value": "0", "value": "support_interface_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and extruders_enabled_count > 1", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and extruders_enabled_count > 1", "settable_per_mesh": false, "settable_per_extruder": false } @@ -4207,7 +4217,7 @@ "gyroid": "Gyroid" }, "default_value": "zigzag", - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -4219,10 +4229,10 @@ "default_value": 1, "minimum_value": "0", "minimum_value_warning": "1 if support_pattern == 'concentric' else 0", - "maximum_value_warning": "3", + "maximum_value_warning": "0 if (support_skip_some_zags and support_pattern == 'zigzag') else 3", "type": "int", "value": "1 if support_tree_enable else (1 if (support_pattern == 'grid' or support_pattern == 'triangles' or support_pattern == 'concentric') else 0)", - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -4234,7 +4244,7 @@ "type": "bool", "default_value": false, "value": "support_pattern == 'cross' or support_pattern == 'gyroid'", - "enabled": "(support_enable or support_tree_enable) and (support_pattern == 'grid' or support_pattern == 'triangles' or support_pattern == 'cross' or support_pattern == 'gyroid')", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and (support_pattern == 'grid' or support_pattern == 'triangles' or support_pattern == 'cross' or support_pattern == 'gyroid')", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -4245,7 +4255,7 @@ "description": "Connect the ZigZags. This will increase the strength of the zig zag support structure.", "type": "bool", "default_value": true, - "enabled": "(support_enable or support_tree_enable) and support_pattern == 'zigzag'", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_pattern == 'zigzag'", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -4260,7 +4270,7 @@ "maximum_value_warning": "100", "default_value": 15, "value": "15 if support_enable else 0 if support_tree_enable else 15", - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true, @@ -4275,7 +4285,7 @@ "minimum_value": "0", "minimum_value_warning": "support_line_width", "default_value": 2.66, - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "value": "0 if support_infill_rate == 0 else (support_line_width * 100) / support_infill_rate * (2 if support_pattern == 'grid' else (3 if support_pattern == 'triangles' else 1))", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, @@ -4290,7 +4300,7 @@ "minimum_value": "0", "minimum_value_warning": "support_line_width", "default_value": 2.66, - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "value": "support_line_distance", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, @@ -4304,7 +4314,7 @@ "description": "A list of integer line directions to use. Elements from the list are used sequentially as the layers progress and when the end of the list is reached, it starts at the beginning again. The list items are separated by commas and the whole list is contained in square brackets. Default is an empty list which means use the default angle 0 degrees.", "type": "[int]", "default_value": "[ ]", - "enabled": "(support_enable or support_tree_enable) and support_pattern != 'concentric' and support_infill_rate > 0", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_pattern != 'concentric' and support_infill_rate > 0", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -4315,7 +4325,7 @@ "description": "Generate a brim within the support infill regions of the first layer. This brim is printed underneath the support, not around it. Enabling this setting increases the adhesion of support to the build plate.", "type": "bool", "default_value": false, - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -4329,7 +4339,7 @@ "default_value": 8.0, "minimum_value": "0.0", "maximum_value_warning": "50.0", - "enabled": "(support_enable or support_tree_enable) and support_brim_enable", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_brim_enable", "settable_per_mesh": false, "settable_per_extruder": true, "limit_to_extruder": "support_infill_extruder_nr", @@ -4344,7 +4354,7 @@ "minimum_value": "0", "maximum_value_warning": "50 / skirt_brim_line_width", "value": "math.ceil(support_brim_width / (skirt_brim_line_width * initial_layer_line_width_factor / 100.0))", - "enabled": "(support_enable or support_tree_enable) and support_brim_enable", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_brim_enable", "settable_per_mesh": false, "settable_per_extruder": true, "limit_to_extruder": "support_infill_extruder_nr" @@ -4361,7 +4371,7 @@ "maximum_value_warning": "machine_nozzle_size", "default_value": 0.1, "limit_to_extruder": "support_interface_extruder_nr if support_interface_enable else support_infill_extruder_nr", - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "settable_per_mesh": true, "children": { @@ -4374,7 +4384,7 @@ "maximum_value_warning": "machine_nozzle_size", "default_value": 0.1, "type": "float", - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "value": "extruderValue(support_roof_extruder_nr if support_roof_enable else support_infill_extruder_nr, 'support_z_distance')", "limit_to_extruder": "support_roof_extruder_nr if support_roof_enable else support_infill_extruder_nr", "settable_per_mesh": true @@ -4390,7 +4400,7 @@ "value": "extruderValue(support_bottom_extruder_nr if support_bottom_enable else support_infill_extruder_nr, 'support_z_distance') if support_type == 'everywhere' else 0", "limit_to_extruder": "support_bottom_extruder_nr if support_bottom_enable else support_infill_extruder_nr", "type": "float", - "enabled": "(support_enable or support_tree_enable) and resolveOrValue('support_type') == 'everywhere'", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and resolveOrValue('support_type') == 'everywhere'", "settable_per_mesh": true } } @@ -4405,7 +4415,7 @@ "maximum_value_warning": "1.5 * machine_nozzle_tip_outer_diameter", "default_value": 0.7, "limit_to_extruder": "support_infill_extruder_nr", - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "settable_per_mesh": true }, "support_xy_overrides_z": @@ -4420,7 +4430,7 @@ }, "default_value": "z_overrides_xy", "limit_to_extruder": "support_infill_extruder_nr", - "enabled": "support_enable", + "enabled": "support_enable or support_meshes_present", "settable_per_mesh": true }, "support_xy_distance_overhang": @@ -4435,7 +4445,7 @@ "default_value": 0.2, "value": "machine_nozzle_size / 2", "limit_to_extruder": "support_infill_extruder_nr", - "enabled": "support_enable and support_xy_overrides_z == 'z_overrides_xy'", + "enabled": "(support_enable or support_meshes_present) and support_xy_overrides_z == 'z_overrides_xy'", "settable_per_mesh": true }, "support_bottom_stair_step_height": @@ -4448,7 +4458,7 @@ "limit_to_extruder": "support_bottom_extruder_nr if support_bottom_enable else support_infill_extruder_nr", "minimum_value": "0", "maximum_value_warning": "1.0", - "enabled": "support_enable", + "enabled": "support_enable or support_meshes_present", "settable_per_mesh": true }, "support_bottom_stair_step_width": @@ -4461,7 +4471,20 @@ "limit_to_extruder": "support_interface_extruder_nr if support_interface_enable else support_infill_extruder_nr", "minimum_value": "0", "maximum_value_warning": "10.0", - "enabled": "support_enable", + "enabled": "support_enable or support_meshes_present", + "settable_per_mesh": true + }, + "support_bottom_stair_step_min_slope": + { + "label": "Support Stair Step Minimum Slope Angle", + "description": "The minimum slope of the area for stair-stepping to take effect. Low values should make support easier to remove on shallower slopes, but really low values may result in some very counter-intuitive results on other parts of the model.", + "unit": "°", + "type": "float", + "default_value": 10.0, + "limit_to_extruder": "support_bottom_extruder_nr if support_bottom_enable else support_infill_extruder_nr", + "minimum_value": "0.01", + "maximum_value": "89.99", + "enabled": "support_enable or support_meshes_present", "settable_per_mesh": true }, "support_join_distance": @@ -4474,7 +4497,7 @@ "limit_to_extruder": "support_infill_extruder_nr", "minimum_value_warning": "0", "maximum_value_warning": "10", - "enabled": "support_enable", + "enabled": "support_enable or support_meshes_present", "settable_per_mesh": true }, "support_offset": @@ -4487,7 +4510,7 @@ "limit_to_extruder": "support_infill_extruder_nr", "minimum_value_warning": "-1 * machine_nozzle_size", "maximum_value_warning": "10 * machine_nozzle_size", - "enabled": "support_enable", + "enabled": "support_enable or support_meshes_present", "settable_per_mesh": true }, "support_infill_sparse_thickness": @@ -4501,7 +4524,7 @@ "maximum_value_warning": "0.75 * machine_nozzle_size", "maximum_value": "resolveOrValue('layer_height') * 8", "value": "resolveOrValue('layer_height')", - "enabled": "(support_enable or support_tree_enable) and support_infill_rate > 0", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_infill_rate > 0", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false }, @@ -4514,7 +4537,7 @@ "minimum_value": "0", "maximum_value_warning": "1 if (support_pattern == 'cross' or support_pattern == 'lines' or support_pattern == 'zigzag' or support_pattern == 'concentric') else 5", "maximum_value": "999999 if support_line_distance == 0 else (20 - math.log(support_line_distance) / math.log(2))", - "enabled": "(support_enable or support_tree_enable) and support_infill_rate > 0", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_infill_rate > 0", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false }, @@ -4527,7 +4550,7 @@ "default_value": 1, "minimum_value": "0.0001", "minimum_value_warning": "3 * resolveOrValue('layer_height')", - "enabled": "(support_enable or support_tree_enable) and support_infill_rate > 0 and gradual_support_infill_steps > 0", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_infill_rate > 0 and gradual_support_infill_steps > 0", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false }, @@ -4540,7 +4563,7 @@ "default_value": 0.0, "minimum_value": "0", "maximum_value_warning": "5", - "enabled": "support_enable", + "enabled": "support_enable or support_meshes_present", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": true }, @@ -4551,7 +4574,7 @@ "type": "bool", "default_value": false, "limit_to_extruder": "support_interface_extruder_nr", - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "settable_per_mesh": true, "children": { @@ -4563,7 +4586,7 @@ "default_value": false, "value": "extruderValue(support_roof_extruder_nr, 'support_interface_enable')", "limit_to_extruder": "support_roof_extruder_nr", - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "settable_per_mesh": true }, "support_bottom_enable": @@ -4574,7 +4597,7 @@ "default_value": false, "value": "extruderValue(support_bottom_extruder_nr, 'support_interface_enable')", "limit_to_extruder": "support_bottom_extruder_nr", - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "settable_per_mesh": true } } @@ -4590,7 +4613,7 @@ "minimum_value_warning": "0.2 + layer_height", "maximum_value_warning": "10", "limit_to_extruder": "support_interface_extruder_nr", - "enabled": "support_interface_enable and (support_enable or support_tree_enable)", + "enabled": "support_interface_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": true, "children": { @@ -4606,7 +4629,7 @@ "maximum_value_warning": "10", "value": "extruderValue(support_roof_extruder_nr, 'support_interface_height')", "limit_to_extruder": "support_roof_extruder_nr", - "enabled": "support_roof_enable and (support_enable or support_tree_enable)", + "enabled": "support_roof_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": true }, "support_bottom_height": @@ -4621,7 +4644,7 @@ "minimum_value_warning": "min(support_bottom_distance + layer_height, support_bottom_stair_step_height)", "maximum_value_warning": "10", "limit_to_extruder": "support_bottom_extruder_nr", - "enabled": "support_bottom_enable and (support_enable or support_tree_enable)", + "enabled": "support_bottom_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": true } } @@ -4635,7 +4658,7 @@ "minimum_value": "0", "maximum_value_warning": "support_interface_height", "limit_to_extruder": "support_interface_extruder_nr", - "enabled": "support_interface_enable and (support_enable or support_tree_enable)", + "enabled": "support_interface_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": true }, "support_interface_density": @@ -4648,7 +4671,7 @@ "minimum_value": "0", "maximum_value_warning": "100", "limit_to_extruder": "support_interface_extruder_nr", - "enabled": "support_interface_enable and (support_enable or support_tree_enable)", + "enabled": "support_interface_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": true, "children": @@ -4663,7 +4686,7 @@ "minimum_value": "0", "maximum_value": "100", "limit_to_extruder": "support_roof_extruder_nr", - "enabled": "support_roof_enable and (support_enable or support_tree_enable)", + "enabled": "support_roof_enable and (support_enable or support_tree_enable or support_meshes_present)", "value": "extruderValue(support_roof_extruder_nr, 'support_interface_density')", "settable_per_mesh": false, "settable_per_extruder": true, @@ -4680,7 +4703,7 @@ "minimum_value_warning": "support_roof_line_width - 0.0001", "value": "0 if support_roof_density == 0 else (support_roof_line_width * 100) / support_roof_density * (2 if support_roof_pattern == 'grid' else (3 if support_roof_pattern == 'triangles' else 1))", "limit_to_extruder": "support_roof_extruder_nr", - "enabled": "support_roof_enable and (support_enable or support_tree_enable)", + "enabled": "support_roof_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": true } @@ -4696,7 +4719,7 @@ "minimum_value": "0", "maximum_value": "100", "limit_to_extruder": "support_bottom_extruder_nr", - "enabled": "support_bottom_enable and (support_enable or support_tree_enable)", + "enabled": "support_bottom_enable and (support_enable or support_tree_enable or support_meshes_present)", "value": "extruderValue(support_bottom_extruder_nr, 'support_interface_density')", "settable_per_mesh": false, "settable_per_extruder": true, @@ -4713,7 +4736,7 @@ "minimum_value_warning": "support_bottom_line_width - 0.0001", "value": "0 if support_bottom_density == 0 else (support_bottom_line_width * 100) / support_bottom_density * (2 if support_bottom_pattern == 'grid' else (3 if support_bottom_pattern == 'triangles' else 1))", "limit_to_extruder": "support_bottom_extruder_nr", - "enabled": "support_bottom_enable and (support_enable or support_tree_enable)", + "enabled": "support_bottom_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": true } @@ -4736,7 +4759,7 @@ }, "default_value": "concentric", "limit_to_extruder": "support_interface_extruder_nr", - "enabled": "support_interface_enable and (support_enable or support_tree_enable)", + "enabled": "support_interface_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": true, "children": @@ -4757,7 +4780,7 @@ "default_value": "concentric", "value": "extruderValue(support_roof_extruder_nr, 'support_interface_pattern')", "limit_to_extruder": "support_roof_extruder_nr", - "enabled": "support_roof_enable and (support_enable or support_tree_enable)", + "enabled": "support_roof_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -4777,7 +4800,7 @@ "default_value": "concentric", "value": "extruderValue(support_bottom_extruder_nr, 'support_interface_pattern')", "limit_to_extruder": "support_bottom_extruder_nr", - "enabled": "support_bottom_enable and (support_enable or support_tree_enable)", + "enabled": "support_bottom_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": true } @@ -4793,7 +4816,7 @@ "minimum_value": "0", "minimum_value_warning": "minimum_support_area", "limit_to_extruder": "support_interface_extruder_nr", - "enabled": "support_interface_enable and (support_enable or support_tree_enable)", + "enabled": "support_interface_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": true, "children": { @@ -4808,7 +4831,7 @@ "minimum_value": "0", "minimum_value_warning": "minimum_support_area", "limit_to_extruder": "support_roof_extruder_nr", - "enabled": "support_roof_enable and (support_enable or support_tree_enable)", + "enabled": "support_roof_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": true }, "minimum_bottom_area": @@ -4822,7 +4845,7 @@ "minimum_value": "0", "minimum_value_warning": "minimum_support_area", "limit_to_extruder": "support_bottom_extruder_nr", - "enabled": "support_bottom_enable and (support_enable or support_tree_enable)", + "enabled": "support_bottom_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": true } } @@ -4836,7 +4859,7 @@ "default_value": 0.0, "maximum_value": "extruderValue(support_extruder_nr, 'support_offset')", "limit_to_extruder": "support_interface_extruder_nr", - "enabled": "support_interface_enable and (support_enable or support_tree_enable)", + "enabled": "support_interface_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": true, "children": @@ -4851,7 +4874,7 @@ "value": "extruderValue(support_roof_extruder_nr, 'support_interface_offset')", "maximum_value": "extruderValue(support_extruder_nr, 'support_offset')", "limit_to_extruder": "support_roof_extruder_nr", - "enabled": "support_roof_enable and (support_enable or support_tree_enable)", + "enabled": "support_roof_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -4865,7 +4888,7 @@ "value": "extruderValue(support_bottom_extruder_nr, 'support_interface_offset')", "maximum_value": "extruderValue(support_extruder_nr, 'support_offset')", "limit_to_extruder": "support_bottom_extruder_nr", - "enabled": "support_bottom_enable and (support_enable or support_tree_enable)", + "enabled": "support_bottom_enable and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": true } @@ -4878,7 +4901,7 @@ "type": "[int]", "default_value": "[ ]", "limit_to_extruder": "support_interface_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and support_interface_enable and support_interface_pattern != 'concentric'", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_interface_enable and support_interface_pattern != 'concentric'", "settable_per_mesh": false, "settable_per_extruder": true, "children": @@ -4891,7 +4914,7 @@ "default_value": "[ ]", "value": "support_interface_angles", "limit_to_extruder": "support_roof_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and support_roof_enable and support_roof_pattern != 'concentric'", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_roof_enable and support_roof_pattern != 'concentric'", "settable_per_mesh": false, "settable_per_extruder": true }, @@ -4903,7 +4926,7 @@ "default_value": "[ ]", "value": "support_interface_angles", "limit_to_extruder": "support_bottom_extruder_nr", - "enabled": "(support_enable or support_tree_enable) and support_bottom_enable and support_bottom_pattern != 'concentric'", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_bottom_enable and support_bottom_pattern != 'concentric'", "settable_per_mesh": false, "settable_per_extruder": true } @@ -4915,7 +4938,7 @@ "description": "When enabled, the print cooling fan speed is altered for the skin regions immediately above the support.", "type": "bool", "default_value": false, - "enabled": "support_enable or support_tree_enable", + "enabled": "support_enable or support_tree_enable or support_meshes_present", "settable_per_mesh": false }, "support_supported_skin_fan_speed": @@ -4927,7 +4950,7 @@ "maximum_value": "100", "default_value": 100, "type": "float", - "enabled": "(support_enable or support_tree_enable) and support_fan_enable", + "enabled": "(support_enable or support_tree_enable or support_meshes_present) and support_fan_enable", "settable_per_mesh": false }, "support_use_towers": @@ -4993,6 +5016,17 @@ "settable_per_extruder": false, "settable_per_meshgroup": false, "settable_globally": false + }, + "support_meshes_present": + { + "label": "Scene Has Support Meshes", + "description": "There are support meshes present in the scene. This setting is controlled by Cura.", + "type": "bool", + "default_value": false, + "enabled": false, + "settable_per_mesh": false, + "settable_per_extruder": false, + "settable_per_meshgroup": false } } }, @@ -5163,7 +5197,7 @@ "description": "Enforce brim to be printed around the model even if that space would otherwise be occupied by support. This replaces some regions of the first layer of support by brim regions.", "type": "bool", "default_value": true, - "enabled": "resolveOrValue('adhesion_type') == 'brim' and (support_enable or support_tree_enable)", + "enabled": "resolveOrValue('adhesion_type') == 'brim' and (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": true, "limit_to_extruder": "support_infill_extruder_nr" @@ -5681,7 +5715,7 @@ "minimum_value": "0", "maximum_value": "min(0.5 * machine_width, 0.5 * machine_depth)", "minimum_value_warning": "max(extruderValues('prime_tower_line_width')) * 2", - "maximum_value_warning": "20", + "maximum_value_warning": "42", "settable_per_mesh": false, "settable_per_extruder": false }, @@ -5947,7 +5981,7 @@ "description": "Remove empty layers beneath the first printed layer if they are present. Disabling this setting can cause empty first layers if the Slicing Tolerance setting is set to Exclusive or Middle.", "type": "bool", "default_value": true, - "enabled": "not (support_enable or support_tree_enable)", + "enabled": "not (support_enable or support_tree_enable or support_meshes_present)", "settable_per_mesh": false, "settable_per_extruder": false }, @@ -6028,8 +6062,8 @@ }, "infill_mesh_order": { - "label": "Infill Mesh Order", - "description": "Determines which infill mesh is inside the infill of another infill mesh. An infill mesh with a higher order will modify the infill of infill meshes with lower order and normal meshes.", + "label": "Mesh Processing Rank", + "description": "Determines the priority of this mesh when considering overlapping volumes. Areas where multiple meshes reside will be won by the lower rank mesh. An infill mesh with a higher order will modify the infill of infill meshes with lower order and normal meshes.", "default_value": 0, "value": "1 if infill_mesh else 0", "minimum_value_warning": "1", @@ -6359,7 +6393,7 @@ "description": "Skip some support line connections to make the support structure easier to break away. This setting is applicable to the Zig Zag support infill pattern.", "type": "bool", "default_value": false, - "enabled": "support_enable and (support_pattern == 'zigzag')", + "enabled": "(support_enable or support_meshes_present) and support_pattern == 'zigzag'", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true @@ -6373,7 +6407,7 @@ "default_value": 20, "minimum_value": "0", "minimum_value_warning": "support_line_distance", - "enabled": "support_enable and (support_pattern == 'zigzag') and support_skip_some_zags", + "enabled": "(support_enable or support_meshes_present) and support_pattern == 'zigzag' and support_skip_some_zags", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true, @@ -6388,7 +6422,7 @@ "value": "0 if support_line_distance == 0 else round(support_skip_zag_per_mm / support_line_distance)", "minimum_value": "1", "minimum_value_warning": "3", - "enabled": "support_enable and (support_pattern == 'zigzag') and support_skip_some_zags", + "enabled": "(support_enable or support_meshes_present) and support_pattern == 'zigzag' and support_skip_some_zags", "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false, "settable_per_extruder": true diff --git a/resources/definitions/geeetech_A10.def.json b/resources/definitions/geeetech_A10.def.json index 6de6653dc0..6e72e4d1c8 100644 --- a/resources/definitions/geeetech_A10.def.json +++ b/resources/definitions/geeetech_A10.def.json @@ -1,58 +1,59 @@ -{ - "version": 2, - "name": "Geeetech A10", - "inherits": "fdmprinter", - "metadata": { - "visible": true, - "author": "Amit L", - "manufacturer": "Geeetech", - "file_formats": "text/x-gcode", - "has_materials": true, - "machine_extruder_trains": - { - "0": "geeetech_A10_1" - } - - }, - - "overrides": { - "machine_name": { "default_value": "Geeetech A10" }, - "machine_width": { - "default_value": 235 - }, - "machine_height": { - "default_value": 260 - }, - "machine_depth": { - "default_value": 235 - }, "machine_center_is_zero": { - "default_value": false - }, - "layer_height": { "default_value": 0.1 }, - "layer_height_0": { "default_value": 0.15 }, - "retraction_amount": { "default_value": 0.8 }, - "retraction_speed": { "default_value": 35 }, - "adhesion_type": { "default_value": "skirt" }, - "machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] }, - "gantry_height": { "value": "28" }, - "machine_max_feedrate_z": { "default_value": 12 }, - "machine_max_feedrate_e": { "default_value": 120 }, - "machine_max_acceleration_z": { "default_value": 500 }, - "machine_acceleration": { "default_value": 1000 }, - "machine_max_jerk_xy": { "default_value": 10 }, - "machine_max_jerk_z": { "default_value": 0.2 }, - "machine_max_jerk_e": { "default_value": 2.5 }, - "machine_heated_bed": { "default_value": true }, - "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, - "machine_start_gcode": { - "default_value": "G28 \nG1 Z15 F300\nM107\nG90\nM82\nM104 S215\nM140 S55\nG92 E0\nM109 S215\nM107\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X180 E40\nG1 F1200 Z2\nG92 E0\nG28" - }, - "machine_end_gcode": { - "default_value": "G91\nG1 E-1\nG0 X0 Y200\nM104 S0\nG90\nG92 E0\nM140 S0\nM84\nM104 S0\nM140 S0\nM84" - }, - "machine_extruder_count": { - "default_value": 1 - } - - } -} +{ + "version": 2, + "name": "Geeetech A10", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Amit L", + "manufacturer": "Geeetech", + "file_formats": "text/x-gcode", + "has_materials": true, + "machine_extruder_trains": + { + "0": "geeetech_A10_1" + } + + }, + + "overrides": { + "machine_name": { "default_value": "Geeetech A10" }, + "machine_width": { + "default_value": 235 + }, + "machine_height": { + "default_value": 260 + }, + "machine_depth": { + "default_value": 235 + }, + "machine_center_is_zero": { + "default_value": false + }, + "layer_height": { "default_value": 0.1 }, + "layer_height_0": { "default_value": 0.15 }, + "retraction_amount": { "default_value": 0.8 }, + "retraction_speed": { "default_value": 35 }, + "adhesion_type": { "default_value": "skirt" }, + "machine_head_with_fans_polygon": { "default_value": [[-31,31],[34,31],[34,-40],[-31,-40]] }, + "gantry_height": { "value": "28" }, + "machine_max_feedrate_z": { "default_value": 12 }, + "machine_max_feedrate_e": { "default_value": 120 }, + "machine_max_acceleration_z": { "default_value": 500 }, + "machine_acceleration": { "default_value": 1000 }, + "machine_max_jerk_xy": { "default_value": 10 }, + "machine_max_jerk_z": { "default_value": 0.2 }, + "machine_max_jerk_e": { "default_value": 2.5 }, + "machine_heated_bed": { "default_value": true }, + "machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" }, + "machine_start_gcode": { + "default_value": "G28 \nG1 Z15 F300\nM107\nG90\nM82\nM104 S215\nM140 S55\nG92 E0\nM109 S215\nM107\nG0 X10 Y20 F6000\nG1 Z0.8\nG1 F300 X180 E40\nG1 F1200 Z2\nG92 E0\nG28" + }, + "machine_end_gcode": { + "default_value": "G91\nG1 E-1\nG0 X0 Y200\nM104 S0\nG90\nG92 E0\nM140 S0\nM84\nM104 S0\nM140 S0\nM84" + }, + "machine_extruder_count": { + "default_value": 1 + } + + } +} diff --git a/resources/definitions/geeetech_A10T.def.json b/resources/definitions/geeetech_A10T.def.json index f989a90982..0f1a0b76ab 100644 --- a/resources/definitions/geeetech_A10T.def.json +++ b/resources/definitions/geeetech_A10T.def.json @@ -27,7 +27,8 @@ }, "machine_depth": { "default_value": 260 - }, "machine_center_is_zero": { + }, + "machine_center_is_zero": { "default_value": false }, "layer_height": { "default_value": 0.1 }, diff --git a/resources/definitions/hms434.def.json b/resources/definitions/hms434.def.json index f749c15a03..cd20906ccc 100644 --- a/resources/definitions/hms434.def.json +++ b/resources/definitions/hms434.def.json @@ -70,7 +70,7 @@ "material_bed_temp_wait": {"default_value": false }, "machine_max_feedrate_z": {"default_value": 10 }, "machine_acceleration": {"default_value": 180 }, - "machine_start_gcode": {"default_value": "\n;Neither Hybrid AM Systems nor any of Hybrid AM Systems representatives has any liabilities or gives any warranties on this .gcode file, or on any or all objects made with this .gcode file.\n\nM140 S{material_bed_temperature_layer_0}\n\nM117 Homing Y ......\nG28 Y\nM117 Homing X ......\nG28 X\nM117 Homing Z ......\nG28 Z F100\n\nG1 Z10 F900\nG1 X-25 Y20 F12000\n\nM190 S{material_bed_temperature_layer_0}\nM117 HMS434 Printing ...\n\nM42 P10 S255 ; chamberfans on" }, + "machine_start_gcode": {"default_value": "\n;Neither Hybrid AM Systems nor any of Hybrid AM Systems representatives has any liabilities or gives any warranties on this .gcode file, or on any or all objects made with this .gcode file.\n\nM114\n\nM140 S{material_bed_temperature_layer_0}\nM118 // action:chamber_fan_on\nM141 S{build_volume_temperature}\n\nM117 Homing Y ......\nG28 Y\nM117 Homing X ......\nG28 X\nM117 Homing Z ......\nG28 Z F100\n\nG1 Z10 F900\nG1 X-25 Y20 F12000\n\nM190 S{material_bed_temperature_layer_0}\n\nM117 HMS434 Printing ..." }, "machine_end_gcode": {"default_value": "" }, "retraction_extra_prime_amount": {"minimum_value_warning": "-2.0" }, @@ -104,7 +104,7 @@ "skin_outline_count": {"value": "0"}, "ironing_line_spacing": {"value": "line_width / 4 * 3"}, "ironing_flow": {"value": "0"}, - "ironing_inset": {"value": "ironing_line_spacing"}, + "ironing_inset": {"value": "ironing_line_spacing + (ironing_line_spacing - skin_line_width * (1.0 + ironing_flow / 100) / 2 if ironing_pattern == 'concentric' else skin_line_width * (1.0 - ironing_flow / 100) / 2)"}, "speed_ironing": {"value": "150"}, "infill_sparse_density": {"value": 30}, diff --git a/resources/definitions/lotmaxx_sc60.def.json b/resources/definitions/lotmaxx_sc60.def.json new file mode 100644 index 0000000000..674431eaab --- /dev/null +++ b/resources/definitions/lotmaxx_sc60.def.json @@ -0,0 +1,72 @@ +{ + "name": "Lotmaxx Shark", + "version": 2, + "inherits": "fdmprinter", + "overrides": { + "machine_name": { "default_value": "Lotmaxx Shark" }, + "machine_width": { "default_value": 235 }, + "machine_depth": { "default_value": 235 }, + "machine_height": { "default_value": 265 }, + "machine_head_with_fans_polygon": { "default_value": [ + [-50.7,16.8], + [-50.7,-29.5], + [46.9,-29.5], + [49.9,16.8] + ] + }, + "gantry_height": { "value": 29 }, + "machine_heated_bed": {"value": true}, + "machine_start_gcode":{ + "default_value":"G28 ;Home\nG92 E0 ;Reset Extruder\nG1 Z4.0 F3000 ;Move Z Axis up\nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder\nG1 Z2.0 F3000 ;Move Z Axis up\n" + }, + "machine_end_gcode":{ + "default_value":"G91 ;Relative positionning\nG1 E-2 F2700 ;Retract a bit\nG1 E-2 Z0.2 F2400 ;Retract and raise Z\nG1 X5 Y5 F3000 ;Wipe out\nG1 Z10 ;Raise Z more\nG90 ;Absolute positionning\n\nG1 X0 Y{machine_depth} ;Present print\nM106 S0 ;Turn-off fan\nM104 S0 ;Turn-off hotend\nM140 S0 ;Turn-off bed\n\nM84 X Y E ;Disable all steppers but Z\n" + }, + "acceleration_print":{"value":1000}, + "acceleration_travel":{"value":1000}, + "acceleration_travel_layer_0":{"value":1000.0}, + "expand_skins_expand_distance":{"value":0.8}, + "fill_outline_gaps":{"default_value":false}, + "infill_sparse_density":{"value":15}, + "meshfix_maximum_resolution":{"value":0.05}, + "optimize_wall_printing_order":{"value":true}, + "retract_at_layer_change":{"value":false}, + "retraction_amount":{"value":4.5}, + "roofing_layer_count":{"value":1}, + "skin_preshrink":{"value":0.8}, + "speed_layer_0":{"value":30}, + "speed_print":{"value":45}, + "speed_roofing":{"value":35}, + "speed_topbottom":{"value":35}, + "speed_travel":{"value":80}, + "speed_wall_0":{"value":32}, + "speed_wall_x":{"value":32}, + "support_infill_rate":{"value":5}, + "support_pattern":{"default_value":"lines"}, + "support_use_towers":{"value":false}, + "wall_overhang_speed_factor":{"value":50}, + "z_seam_corner":{"default_value":"z_seam_corner_any"}, + "z_seam_relative":{"value":true}, + "z_seam_type":{"default_value":"sharpest_corner"}, + "zig_zaggify_infill":{"value":true}, + "adhesion_type":{"default_value":"skirt"}, + "prime_tower_enable":{"value":true}, + "prime_tower_position_x":{"value": 50}, + "prime_tower_position_y":{"value": 50}, + "prime_tower_min_volume":{"value": 30}, + "switch_extruder_retraction_amount": {"value": 100}, + "switch_extruder_retraction_speeds": {"value": 60} + }, + "metadata": { + "visible": true, + "author": "lotmaxx.com", + "manufacturer": "Lotmaxx", + "platform": "lotmaxx_sc_10_20_platform.3mf", + "machine_extruder_trains": { + "0": "lotmaxx_sc60_extruder_left", + "1": "lotmaxx_sc60_extruder_right" + }, + "has_materials": true, + "preferred_quality_type": "normal" + } +} diff --git a/resources/definitions/skriware_2.def.json b/resources/definitions/skriware_2.def.json index c4449129b7..ab5532db81 100644 --- a/resources/definitions/skriware_2.def.json +++ b/resources/definitions/skriware_2.def.json @@ -345,7 +345,7 @@ "value": "0.8" }, "ironing_inset": { - "value": "0.2" + "value": "0.2 + (ironing_line_spacing - skin_line_width * (1.0 + ironing_flow / 100) / 2 if ironing_pattern == 'concentric' else skin_line_width * (1.0 - ironing_flow / 100) / 2)" }, "jerk_travel": { "value": "10" diff --git a/resources/definitions/smoothie.def.json b/resources/definitions/smoothie.def.json new file mode 100644 index 0000000000..463fbc92c2 --- /dev/null +++ b/resources/definitions/smoothie.def.json @@ -0,0 +1,37 @@ +{ + "version": 2, + "name": "Smoothie Custom Printer", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "grk3010", + "manufacturer": "Custom", + "file_formats": "text/x-gcode", + "machine_extruder_trains": + { + "0": "custom_extruder_1", + "1": "custom_extruder_2", + "2": "custom_extruder_3", + "3": "custom_extruder_4", + "4": "custom_extruder_5", + "5": "custom_extruder_6", + "6": "custom_extruder_7", + "7": "custom_extruder_8" + }, + "first_start_actions": ["MachineSettingsAction"] + }, + "overrides": { + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "machine_always_write_active_tool": { + "default_value": true + }, + "machine_start_gcode": { + "default_value": "G28 X0 Y0 ; Home X Y\nM375; Load Bed Leveling Grid\nG1 X300 Y275 F15000 ; Move to bed center\nM280 S3.0 ; Deploy probe pin\n## NOTE: Adjust \"ZX.XX\" after G30 to be the Z-offset (in mm) between the probe and the nozzle\n## This is how you adjust nozzle printing height!\nG30 Z1.25 ; Move down to find bed & set Z home offset\nM280 S7.0 ; Retract probe\nG1 Z15.0 F600 ; move extruder up 15mm\nT0; Activate Extruder 1" + }, + "machine_end_gcode": { + "default_value": "G28 X0 Y0; Home X and Y\nM104 S0 ; turn off extruder\nM140 S0 ; turn off bed\nM107; turn off fans\nM84 ; disable motors" + } + } +} diff --git a/resources/definitions/tevo_tarantula.def.json b/resources/definitions/tevo_tarantula.def.json index a12bff24f8..eee773cd74 100644 --- a/resources/definitions/tevo_tarantula.def.json +++ b/resources/definitions/tevo_tarantula.def.json @@ -11,7 +11,8 @@ "platform": "prusai3_platform.3mf", "machine_extruder_trains": { - "0": "tevo_tarantula_extruder_0" + "0": "tevo_tarantula_extruder_0", + "1": "tevo_tarantula_extruder_1" } }, diff --git a/resources/definitions/tevo_tarantula_pro.def.json b/resources/definitions/tevo_tarantula_pro.def.json index b53c59062b..793942a50b 100644 --- a/resources/definitions/tevo_tarantula_pro.def.json +++ b/resources/definitions/tevo_tarantula_pro.def.json @@ -11,7 +11,8 @@ "has_materials": true, "machine_extruder_trains": { - "0": "tevo_tarantula_pro_extruder_0" + "0": "tevo_tarantula_pro_extruder_0", + "1": "tevo_tarantula_pro_extruder_1" } }, diff --git a/resources/definitions/tinyboy_e10.def.json b/resources/definitions/tinyboy_e10.def.json new file mode 100644 index 0000000000..26c306cf4e --- /dev/null +++ b/resources/definitions/tinyboy_e10.def.json @@ -0,0 +1,31 @@ +{ + "version": 2, + "name": "TinyBoy E10/J10/L10/M10", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Fred Chan", + "manufacturer": "TinyBoy", + "file_formats": "text/x-gcode", + "has_materials": false, + "has_machine_quality": true, + "preferred_quality_type": "normal", + "machine_extruder_trains": + { + "0": "tinyboy_extruder_0" + } + }, + + "overrides": { + "machine_name": { "default_value": "TinyBoy E10" }, + "machine_width": { "default_value": 100 }, + "machine_depth": { "default_value": 100 }, + "machine_height": { "default_value": 105 }, + "machine_start_gcode": { + "default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform" + }, + "machine_end_gcode": { + "default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84" + } + } +} diff --git a/resources/definitions/tinyboy_e16.def.json b/resources/definitions/tinyboy_e16.def.json new file mode 100644 index 0000000000..7f63405c79 --- /dev/null +++ b/resources/definitions/tinyboy_e16.def.json @@ -0,0 +1,31 @@ +{ + "version": 2, + "name": "TinyBoy E16/L16/M16", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Fred Chan", + "manufacturer": "TinyBoy", + "file_formats": "text/x-gcode", + "has_materials": false, + "has_machine_quality": true, + "preferred_quality_type": "normal", + "machine_extruder_trains": + { + "0": "tinyboy_extruder_0" + } + }, + + "overrides": { + "machine_name": { "default_value": "TinyBoy E16" }, + "machine_width": { "default_value": 100 }, + "machine_depth": { "default_value": 100 }, + "machine_height": { "default_value": 165 }, + "machine_start_gcode": { + "default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform" + }, + "machine_end_gcode": { + "default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84" + } + } +} diff --git a/resources/definitions/tinyboy_ra20.def.json b/resources/definitions/tinyboy_ra20.def.json new file mode 100644 index 0000000000..9f1e4c9071 --- /dev/null +++ b/resources/definitions/tinyboy_ra20.def.json @@ -0,0 +1,36 @@ +{ + "version": 2, + "name": "TinyBoy RA20", + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Fred Chan", + "manufacturer": "TinyBoy", + "file_formats": "text/x-gcode", + "platform": "tinyboy_ra20.obj", + "platform_offset": [ 8, -73.3, -8 ], + "has_materials": false, + "has_machine_quality": true, + "preferred_quality_type": "normal", + "machine_extruder_trains": + { + "0": "tinyboy_extruder_0" + } + }, + + "overrides": { + "machine_name": { "default_value": "TinyBoy RA20" }, + "machine_width": { "default_value": 120 }, + "machine_depth": { "default_value": 120 }, + "machine_height": { "default_value": 205 }, + "machine_heated_bed": { "default_value": true }, + + "machine_start_gcode": { + "default_value": "G28 ;Home\nG1 Z15.0 F2000 ;Move the platform" + }, + "machine_end_gcode": { + "default_value": "M104 S0\nM140 S0\nG92 E80\nG1 E-80 F2000\nG28 X0 Y0\nM84" + } + } +} + diff --git a/resources/definitions/ultimaker2.def.json b/resources/definitions/ultimaker2.def.json index 68b41feeb0..3980baba31 100644 --- a/resources/definitions/ultimaker2.def.json +++ b/resources/definitions/ultimaker2.def.json @@ -23,7 +23,7 @@ "overrides": { "machine_name": { "default_value": "Ultimaker 2" }, "machine_start_gcode" : { - "value": "\"\" if machine_gcode_flavor == \"UltiGCode\" else \"G21 ;metric values\\nG90 ;absolute positioning\\nM82 ;set extruder to absolute mode\\nM107 ;start with the fan off\\nG28 Z0 ;move Z to bottom endstops\\nG28 X0 Y0 ;move X/Y to endstops\\nG1 X15 Y0 F4000 ;move X/Y to front of printer\\nG1 Z15.0 F9000 ;move the platform to 15mm\\nG92 E0 ;zero the extruded length\\nG1 F200 E10 ;extrude 10 mm of feed stock\\nG92 E0 ;zero the extruded length again\\nG1 F9000\\n;Put printing message on LCD screen\\nM117 Printing...\"" + "value": "\"G0 F3000 Y50 ;avoid prime blob\" if machine_gcode_flavor == \"UltiGCode\" else \"G21 ;metric values\\nG90 ;absolute positioning\\nM82 ;set extruder to absolute mode\\nM107 ;start with the fan off\\nG28 Z0 ;move Z to bottom endstops\\nG28 X0 Y0 ;move X/Y to endstops\\nG1 X15 Y0 F4000 ;move X/Y to front of printer\\nG1 Z15.0 F9000 ;move the platform to 15mm\\nG92 E0 ;zero the extruded length\\nG1 F200 E10 ;extrude 10 mm of feed stock\\nG92 E0 ;zero the extruded length again\\nG1 Y50 F9000\\n;Put printing message on LCD screen\\nM117 Printing...\"" }, "machine_end_gcode" : { "value": "\";Version _2.6 of the firmware can abort the print too early if the file ends\\n;too soon. However if the file hasn't ended yet because there are comments at\\n;the end of the file, it won't abort yet. Therefore we have to put at least 512\\n;bytes at the end of the g-code so that the file is not yet finished by the\\n;time that the motion planner gets flushed. With firmware version _3.3 this\\n;should be fixed, so this comment wouldn't be necessary any more. Now we have\\n;to pad this text to make precisely 512 bytes.\" if machine_gcode_flavor == \"UltiGCode\" else \"M104 S0 ;extruder heater off\\nM140 S0 ;heated bed heater off (if you have it)\\nG91 ;relative positioning\\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\\nM84 ;steppers off\\nG90 ;absolute positioning\\n;Version _2.6 of the firmware can abort the print too early if the file ends\\n;too soon. However if the file hasn't ended yet because there are comments at\\n;the end of the file, it won't abort yet. Therefore we have to put at least 512\\n;bytes at the end of the g-code so that the file is not yet finished by the\\n;time that the motion planner gets flushed. With firmware version _3.3 this\\n;should be fixed, so this comment wouldn't be necessary any more. Now we have\\n;to pad this text to make precisely 512 bytes.\"" diff --git a/resources/definitions/ultimaker_original.def.json b/resources/definitions/ultimaker_original.def.json index 10359f2fe6..34d22934d6 100644 --- a/resources/definitions/ultimaker_original.def.json +++ b/resources/definitions/ultimaker_original.def.json @@ -55,7 +55,7 @@ "default_value": "RepRap (Marlin/Sprinter)" }, "machine_start_gcode": { - "default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG1 Z15.0 F9000 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E6 ;extrude 6 mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F9000\n;Put printing message on LCD screen\nM117 Printing..." + "default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG1 Z15.0 F9000 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E6 ;extrude 6 mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 Y50 F9000\n;Put printing message on LCD screen\nM117 Printing..." }, "machine_end_gcode": { "value": "'M104 S0 ;extruder heater off' + ('\\nM140 S0 ;heated bed heater off' if machine_heated_bed else '') + '\\nG91 ;relative positioning\\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\\nM84 ;steppers off\\nG90 ;absolute positioning'" diff --git a/resources/extruders/I3MetalMotion_extruder_0.def.json b/resources/extruders/I3MetalMotion_extruder_0.def.json new file mode 100644 index 0000000000..064e1360b6 --- /dev/null +++ b/resources/extruders/I3MetalMotion_extruder_0.def.json @@ -0,0 +1,17 @@ +{ + + "version": 2, + "name": "I3MetalMotion extruder", + "inherits": "fdmextruder", + "metadata": { + "machine": "I3MetalMotion", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "material_diameter": { "default_value": 1.75 }, + "machine_nozzle_offset_x": { "default_value": 0 }, + "machine_nozzle_offset_y": { "default_value": 0 } + } +} diff --git a/resources/extruders/SV01_extruder_0.def.json b/resources/extruders/SV01_extruder_0.def.json new file mode 100644 index 0000000000..1620eb0b1d --- /dev/null +++ b/resources/extruders/SV01_extruder_0.def.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "SV01", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/extruders/SV02_extruder_0.def.json b/resources/extruders/SV02_extruder_0.def.json new file mode 100644 index 0000000000..073242f2bd --- /dev/null +++ b/resources/extruders/SV02_extruder_0.def.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "name": "Left Extruder", + "inherits": "fdmextruder", + "metadata": { + "machine": "SV02", + "position": "0" + }, + + "overrides": { + "extruder_nr": { + "default_value": 0, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { "default_value": 0.0 }, + "machine_nozzle_offset_y": { "default_value": 0.0 }, + "material_diameter": { "default_value": 1.75 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "machine_extruder_start_code": { + "default_value": "\nT0 ;switch to extruder 1\nG92 E0 ;reset extruder distance\nG1 F2000 E93 ;load filament\nG92 E0 ;reset extruder distance\nM104 S{material_print_temperature}" + }, + "machine_extruder_end_code": { + "default_value": "\nG92 E0 ;reset extruder distance\nG1 F800 E-5 ;short retract\nG1 F2400 X250 Y220\nG1 F2000 E-93 ;long retract for filament removal\nG92 E0 ;reset extruder distance\nG90" + } + } +} diff --git a/resources/extruders/SV02_extruder_1.def.json b/resources/extruders/SV02_extruder_1.def.json new file mode 100644 index 0000000000..41ede774df --- /dev/null +++ b/resources/extruders/SV02_extruder_1.def.json @@ -0,0 +1,26 @@ +{ + "version": 2, + "name": "Right Extruder", + "inherits": "fdmextruder", + "metadata": { + "machine": "SV02", + "position": "1" + }, + + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { "default_value": 0.0 }, + "machine_nozzle_offset_y": { "default_value": 0.0 }, + "material_diameter": { "default_value": 1.75 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "machine_extruder_start_code": { + "default_value": "\nT1 ;switch to extruder 2\nG92 E0 ;reset extruder distance\nG1 F2000 E93 ;load filament\nG92 E0 ;reset extruder distance\nM104 S{material_print_temperature}" + }, + "machine_extruder_end_code": { + "default_value": "\nG92 E0 ;reset extruder distance\nG1 F800 E-5 ;short retract\nG1 F2400 X250 Y220\nG1 F2000 E-93 ;long retract for filament removal\nG92 E0 ;reset extruder distance\nG90" + } + } +} diff --git a/resources/extruders/atmat_signal_pro_extruder_left.def.json b/resources/extruders/atmat_signal_pro_extruder_left.def.json new file mode 100644 index 0000000000..1e379ec875 --- /dev/null +++ b/resources/extruders/atmat_signal_pro_extruder_left.def.json @@ -0,0 +1,28 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "atmat_signal_pro_base", + "position": "0" + }, + + "overrides": { + "extruder_nr": { + "default_value": 0, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { "default_value": 0 }, + "machine_nozzle_offset_y": { "default_value": 0 }, + "machine_extruder_start_pos_abs": { "default_value": true }, + "machine_extruder_start_pos_x": { "default_value": 270 }, + "machine_extruder_start_pos_y": { "default_value": 270 }, + "machine_extruder_end_pos_abs": { "default_value": true }, + "machine_extruder_end_pos_x": { "default_value": 270 }, + "machine_extruder_end_pos_y": { "default_value": 270 }, + "extruder_prime_pos_x": { "default_value": 0 }, + "extruder_prime_pos_y": { "default_value": 0 }, + "extruder_prime_pos_z": { "default_value": 0 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/extruders/atmat_signal_pro_extruder_right.def.json b/resources/extruders/atmat_signal_pro_extruder_right.def.json new file mode 100644 index 0000000000..7eef618a1e --- /dev/null +++ b/resources/extruders/atmat_signal_pro_extruder_right.def.json @@ -0,0 +1,28 @@ +{ + "version": 2, + "name": "Extruder 2", + "inherits": "fdmextruder", + "metadata": { + "machine": "atmat_signal_pro_base", + "position": "1" + }, + + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "1" + }, + "machine_nozzle_offset_x": { "default_value": 0 }, + "machine_nozzle_offset_y": { "default_value": 0 }, + "machine_extruder_start_pos_abs": { "default_value": true }, + "machine_extruder_start_pos_x": { "default_value": 270 }, + "machine_extruder_start_pos_y": { "default_value": 270 }, + "machine_extruder_end_pos_abs": { "default_value": true }, + "machine_extruder_end_pos_x": { "default_value": 270 }, + "machine_extruder_end_pos_y": { "default_value": 270 }, + "extruder_prime_pos_x": { "default_value": 0 }, + "extruder_prime_pos_y": { "default_value": 0 }, + "extruder_prime_pos_z": { "default_value": 0 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/extruders/dagoma_discoeasy200_extruder.def.json b/resources/extruders/dagoma_discoeasy200_extruder.def.json new file mode 100644 index 0000000000..7bef0a7649 --- /dev/null +++ b/resources/extruders/dagoma_discoeasy200_extruder.def.json @@ -0,0 +1,21 @@ +{ + "version": 2, + "name": "Extruder", + "inherits": "fdmextruder", + "metadata": { + "machine": "dagoma_discoeasy200", + "position": "0" + }, + + "overrides": { + "extruder_nr": { + "default_value": 0 + }, + "machine_nozzle_size": { + "default_value": 0.4 + }, + "material_diameter": { + "default_value": 1.75 + } + } +} diff --git a/resources/extruders/dagoma_discoeasy200_extruder_0.def.json b/resources/extruders/dagoma_discoeasy200_extruder_0.def.json index f2ca729582..af1c2c42e9 100644 --- a/resources/extruders/dagoma_discoeasy200_extruder_0.def.json +++ b/resources/extruders/dagoma_discoeasy200_extruder_0.def.json @@ -3,7 +3,7 @@ "name": "Extruder 1", "inherits": "fdmextruder", "metadata": { - "machine": "dagoma_discoeasy200", + "machine": "dagoma_discoeasy200_bicolor", "position": "0" }, @@ -18,10 +18,10 @@ "default_value": 1.75 }, "machine_extruder_start_code": { - "default_value": "\n;Start T0\nG92 E0\nG1 E-{retraction_amount} F10000\nG92 E0G1 E1.5 F3000\nG1 E-60 F10000\nG92 E0\n" + "default_value": ";Start T0\nM83\nG1 E{retraction_amount} F3000\nG1 E60 F3000\nG1 E-{retraction_amount} F5000\nM82\nG92 E0" }, "machine_extruder_end_code": { - "default_value": "\nG92 E0\nG1 E{retraction_amount} F3000\nG92 E0\nG1 E60 F3000\nG92 E0\nG1 E-{retraction_amount} F5000\n;End T0\n\n" + "default_value": "M83\nG1 E-{retraction_amount} F10000\nG1 E1.5 F3000\nG1 E-60 F10000\nM82\nG92 E0\n;End T0" } } } diff --git a/resources/extruders/dagoma_discoeasy200_extruder_1.def.json b/resources/extruders/dagoma_discoeasy200_extruder_1.def.json index ac5fe5015d..f01a805270 100644 --- a/resources/extruders/dagoma_discoeasy200_extruder_1.def.json +++ b/resources/extruders/dagoma_discoeasy200_extruder_1.def.json @@ -3,7 +3,7 @@ "name": "Extruder 2", "inherits": "fdmextruder", "metadata": { - "machine": "dagoma_discoeasy200", + "machine": "dagoma_discoeasy200_bicolor", "position": "1" }, @@ -18,10 +18,10 @@ "default_value": 1.75 }, "machine_extruder_start_code": { - "default_value": "\n;Start T1\nG92 E0\nG1 E-{retraction_amount} F10000\nG92 E0G1 E1.5 F3000\nG1 E-60 F10000\nG92 E0\n" + "default_value": ";Start T1\nM83\nG1 E{retraction_amount} F3000\nG1 E60 F3000\nG1 E-{retraction_amount} F5000\nM82\nG92 E0" }, "machine_extruder_end_code": { - "default_value": "\nG92 E0\nG1 E{retraction_amount} F3000\nG92 E0\nG1 E60 F3000\nG92 E0\nG1 E-{retraction_amount} F5000\n;End T1\n\n" + "default_value": "M83\nG1 E-{retraction_amount} F10000\nG1 E1.5 F3000\nG1 E-60 F10000\nM82\nG92 E0\n;End T1" } } } diff --git a/resources/extruders/dagoma_discoultimate_extruder.def.json b/resources/extruders/dagoma_discoultimate_extruder.def.json new file mode 100644 index 0000000000..a53145f429 --- /dev/null +++ b/resources/extruders/dagoma_discoultimate_extruder.def.json @@ -0,0 +1,21 @@ +{ + "version": 2, + "name": "Extruder", + "inherits": "fdmextruder", + "metadata": { + "machine": "dagoma_discoultimate", + "position": "0" + }, + + "overrides": { + "extruder_nr": { + "default_value": 0 + }, + "machine_nozzle_size": { + "default_value": 0.4 + }, + "material_diameter": { + "default_value": 1.75 + } + } +} diff --git a/resources/extruders/dagoma_discoultimate_extruder_0.def.json b/resources/extruders/dagoma_discoultimate_extruder_0.def.json index 878d6fdc75..766d5b2902 100644 --- a/resources/extruders/dagoma_discoultimate_extruder_0.def.json +++ b/resources/extruders/dagoma_discoultimate_extruder_0.def.json @@ -3,7 +3,7 @@ "name": "Extruder 1", "inherits": "fdmextruder", "metadata": { - "machine": "dagoma_discoultimate", + "machine": "dagoma_discoultimate_bicolor", "position": "0" }, @@ -18,10 +18,10 @@ "default_value": 1.75 }, "machine_extruder_start_code": { - "default_value": "\n;Start T0\nG92 E0\nG1 E-{retraction_amount} F10000\nG92 E0G1 E1.5 F3000\nG1 E-60 F10000\nG92 E0\n" + "default_value": ";Start T0\nM83\nG1 E{retraction_amount} F3000\nG1 E60 F3000\nG1 E-{retraction_amount} F5000\nM82\nG92 E0" }, "machine_extruder_end_code": { - "default_value": "\nG92 E0\nG1 E{retraction_amount} F3000\nG92 E0\nG1 E60 F3000\nG92 E0\nG1 E-{retraction_amount} F5000\n;End T0\n\n" + "default_value": "M83\nG1 E-{retraction_amount} F10000\nG1 E1.5 F3000\nG1 E-60 F10000\nM82\nG92 E0\n;End T0" } } } diff --git a/resources/extruders/dagoma_discoultimate_extruder_1.def.json b/resources/extruders/dagoma_discoultimate_extruder_1.def.json index e6f8031e03..f71b875261 100644 --- a/resources/extruders/dagoma_discoultimate_extruder_1.def.json +++ b/resources/extruders/dagoma_discoultimate_extruder_1.def.json @@ -3,7 +3,7 @@ "name": "Extruder 2", "inherits": "fdmextruder", "metadata": { - "machine": "dagoma_discoultimate", + "machine": "dagoma_discoultimate_bicolor", "position": "1" }, @@ -18,10 +18,10 @@ "default_value": 1.75 }, "machine_extruder_start_code": { - "default_value": "\n;Start T1\nG92 E0\nG1 E-{retraction_amount} F10000\nG92 E0G1 E1.5 F3000\nG1 E-60 F10000\nG92 E0\n" + "default_value": ";Start T1\nM83\nG1 E{retraction_amount} F3000\nG1 E60 F3000\nG1 E-{retraction_amount} F5000\nM82\nG92 E0" }, "machine_extruder_end_code": { - "default_value": "\nG92 E0\nG1 E{retraction_amount} F3000\nG92 E0\nG1 E60 F3000\nG92 E0\nG1 E-{retraction_amount} F5000\n;End T1\n\n" + "default_value": "M83\nG1 E-{retraction_amount} F10000\nG1 E1.5 F3000\nG1 E-60 F10000\nM82\nG92 E0\n;End T1" } } } diff --git a/resources/extruders/dagoma_magis_extruder_0.def.json b/resources/extruders/dagoma_magis_extruder.def.json similarity index 93% rename from resources/extruders/dagoma_magis_extruder_0.def.json rename to resources/extruders/dagoma_magis_extruder.def.json index 0a5850f2ed..06cf6127ce 100644 --- a/resources/extruders/dagoma_magis_extruder_0.def.json +++ b/resources/extruders/dagoma_magis_extruder.def.json @@ -1,6 +1,6 @@ { "version": 2, - "name": "Extruder 1", + "name": "Extruder", "inherits": "fdmextruder", "metadata": { "machine": "dagoma_magis", diff --git a/resources/extruders/dagoma_neva_extruder_0.def.json b/resources/extruders/dagoma_neva_extruder.def.json similarity index 93% rename from resources/extruders/dagoma_neva_extruder_0.def.json rename to resources/extruders/dagoma_neva_extruder.def.json index 95035f63f2..3fa26ab5c9 100644 --- a/resources/extruders/dagoma_neva_extruder_0.def.json +++ b/resources/extruders/dagoma_neva_extruder.def.json @@ -1,6 +1,6 @@ { "version": 2, - "name": "Extruder 1", + "name": "Extruder", "inherits": "fdmextruder", "metadata": { "machine": "dagoma_neva", diff --git a/resources/extruders/hms434_tool_1.def.json b/resources/extruders/hms434_tool_1.def.json index e37ee0abfc..2a36dd4c2b 100644 --- a/resources/extruders/hms434_tool_1.def.json +++ b/resources/extruders/hms434_tool_1.def.json @@ -16,7 +16,7 @@ "machine_nozzle_offset_y": { "default_value": 0.0 }, "material_diameter": { "default_value": 1.75 }, "machine_extruder_start_code": { - "default_value": "\n;changing to tool1\nM83\nM109 T0 S{material_print_temperature}\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E-{switch_extruder_retraction_amount} F2400\nG1 Y40 F3000\nG1 X10 F12000\n\n" + "default_value": "\n;changing to tool1\nM83\nM109 T0 S{material_print_temperature}\nM114\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E-{switch_extruder_retraction_amount} F2400\nG1 Y40 F3000\nG1 X10 F12000\n\n" }, "machine_extruder_end_code": { "default_value": "\nG1 X10 Y40 F12000\nG1 X-25 F12000\nM109 T0 R{material_standby_temperature}\nG1 Y20 F3000\n; ending tool1\n\n" diff --git a/resources/extruders/hms434_tool_2.def.json b/resources/extruders/hms434_tool_2.def.json index 2300331e39..110695a8fc 100644 --- a/resources/extruders/hms434_tool_2.def.json +++ b/resources/extruders/hms434_tool_2.def.json @@ -16,7 +16,7 @@ "machine_nozzle_offset_y": { "default_value": 0.0 }, "material_diameter": { "default_value": 1.75 }, "machine_extruder_start_code": { - "default_value": "\n;changing to tool2\nM83\nM109 T1 S{material_print_temperature}\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E-{switch_extruder_retraction_amount} F2400\nG1 Y40 F3000\nG1 X10 F12000\n\n" + "default_value": "\n;changing to tool2\nM83\nM109 T1 S{material_print_temperature}\nM114\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E{switch_extruder_retraction_amount} F300\nG1 E-{switch_extruder_retraction_amount} F2400\nG1 Y40 F3000\nG1 X10 F12000\n\n" }, "machine_extruder_end_code": { "default_value": "\nG1 X10 Y40 F12000\nG1 X-25 F12000\nM109 T1 R{material_standby_temperature}\nG1 Y20 F3000\n; ending tool2\n\n" diff --git a/resources/extruders/lotmaxx_sc60_extruder_left.def.json b/resources/extruders/lotmaxx_sc60_extruder_left.def.json new file mode 100644 index 0000000000..430cea8106 --- /dev/null +++ b/resources/extruders/lotmaxx_sc60_extruder_left.def.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "lotmaxx_sc60", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/extruders/lotmaxx_sc60_extruder_right.def.json b/resources/extruders/lotmaxx_sc60_extruder_right.def.json new file mode 100644 index 0000000000..a5a96c993b --- /dev/null +++ b/resources/extruders/lotmaxx_sc60_extruder_right.def.json @@ -0,0 +1,17 @@ +{ + "version": 2, + "name": "Extruder 2", + "inherits": "fdmextruder", + "metadata": { + "machine": "lotmaxx_sc60", + "position": "1" + }, + "overrides": { + "extruder_nr": { + "default_value": 1, + "maximum_value": "1" + }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/extruders/tevo_tarantula_extruder_1.def.json b/resources/extruders/tevo_tarantula_extruder_1.def.json new file mode 100644 index 0000000000..20d4c7004a --- /dev/null +++ b/resources/extruders/tevo_tarantula_extruder_1.def.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "name": "Extruder 2", + "inherits": "fdmextruder", + "metadata": { + "machine": "tevo_tarantula", + "position": "1" + }, + + "overrides": { + "extruder_nr": { "default_value": 1 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/extruders/tevo_tarantula_pro_extruder_1.def.json b/resources/extruders/tevo_tarantula_pro_extruder_1.def.json new file mode 100644 index 0000000000..f02e74efff --- /dev/null +++ b/resources/extruders/tevo_tarantula_pro_extruder_1.def.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "name": "Extruder 2", + "inherits": "fdmextruder", + "metadata": { + "machine": "tevo_tarantula_pro", + "position": "1" + }, + + "overrides": { + "extruder_nr": { "default_value": 1 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/extruders/tinyboy_extruder_0.def.json b/resources/extruders/tinyboy_extruder_0.def.json new file mode 100644 index 0000000000..246d00135e --- /dev/null +++ b/resources/extruders/tinyboy_extruder_0.def.json @@ -0,0 +1,15 @@ +{ + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "tinyboy_e10", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.3 }, + "material_diameter": { "default_value": 1.75 } + } +} diff --git a/resources/i18n/fr_FR/fdmprinter.def.json.po b/resources/i18n/fr_FR/fdmprinter.def.json.po index 30940b9614..9a20bb91ec 100644 --- a/resources/i18n/fr_FR/fdmprinter.def.json.po +++ b/resources/i18n/fr_FR/fdmprinter.def.json.po @@ -852,12 +852,12 @@ msgstr "Largeur d'une seule ligne de bas de support." #: fdmprinter.def.json msgctxt "prime_tower_line_width label" msgid "Prime Tower Line Width" -msgstr "Largeur de ligne de la tour primaire" +msgstr "Largeur de ligne de la tour d'amorçage" #: fdmprinter.def.json msgctxt "prime_tower_line_width description" msgid "Width of a single prime tower line." -msgstr "Largeur d'une seule ligne de tour primaire." +msgstr "Largeur d'une seule ligne de tour d'amorçage." #: fdmprinter.def.json msgctxt "initial_layer_line_width_factor label" @@ -2369,12 +2369,12 @@ msgstr "Compensation de débit sur les lignes de bas de support." #: fdmprinter.def.json msgctxt "prime_tower_flow label" msgid "Prime Tower Flow" -msgstr "Débit de la tour primaire" +msgstr "Débit de la tour d'amorçage" #: fdmprinter.def.json msgctxt "prime_tower_flow description" msgid "Flow compensation on prime tower lines." -msgstr "Compensation de débit sur les lignes de la tour primaire." +msgstr "Compensation de débit sur les lignes de la tour d'amorçage." #: fdmprinter.def.json msgctxt "material_flow_layer_0 label" @@ -2529,12 +2529,12 @@ msgstr "La vitesse à laquelle le bas de support est imprimé. L'impression à u #: fdmprinter.def.json msgctxt "speed_prime_tower label" msgid "Prime Tower Speed" -msgstr "Vitesse de la tour primaire" +msgstr "Vitesse de la tour d'amorçage" #: fdmprinter.def.json msgctxt "speed_prime_tower description" msgid "The speed at which the prime tower is printed. Printing the prime tower slower can make it more stable when the adhesion between the different filaments is suboptimal." -msgstr "La vitesse à laquelle la tour primaire est imprimée. L'impression plus lente de la tour primaire peut la rendre plus stable lorsque l'adhérence entre les différents filaments est sous-optimale." +msgstr "La vitesse à laquelle la tour d'amorçage est imprimée. L'impression plus lente de la tour d'amorçage peut la rendre plus stable lorsque l'adhérence entre les différents filaments est sous-optimale." #: fdmprinter.def.json msgctxt "speed_travel label" @@ -2759,12 +2759,12 @@ msgstr "L'accélération selon laquelle les bas de support sont imprimés. Les i #: fdmprinter.def.json msgctxt "acceleration_prime_tower label" msgid "Prime Tower Acceleration" -msgstr "Accélération de la tour primaire" +msgstr "Accélération de la tour d'amorçage" #: fdmprinter.def.json msgctxt "acceleration_prime_tower description" msgid "The acceleration with which the prime tower is printed." -msgstr "L'accélération selon laquelle la tour primaire est imprimée." +msgstr "L'accélération selon laquelle la tour d'amorçage est imprimée." #: fdmprinter.def.json msgctxt "acceleration_travel label" @@ -2949,12 +2949,12 @@ msgstr "Le changement instantané maximal de vitesse selon lequel les bas de sup #: fdmprinter.def.json msgctxt "jerk_prime_tower label" msgid "Prime Tower Jerk" -msgstr "Saccade de la tour primaire" +msgstr "Saccade de la tour d'amorçage" #: fdmprinter.def.json msgctxt "jerk_prime_tower description" msgid "The maximum instantaneous velocity change with which the prime tower is printed." -msgstr "Le changement instantané maximal de vitesse selon lequel la tour primaire est imprimée." +msgstr "Le changement instantané maximal de vitesse selon lequel la tour d'amorçage est imprimée." #: fdmprinter.def.json msgctxt "jerk_travel label" @@ -3069,7 +3069,7 @@ msgstr "La vitesse à laquelle le filament est rétracté pendant une rétractio #: fdmprinter.def.json msgctxt "retraction_prime_speed label" msgid "Retraction Prime Speed" -msgstr "Vitesse de rétraction primaire" +msgstr "Vitesse de rétraction d'amorçage" #: fdmprinter.def.json msgctxt "retraction_prime_speed description" @@ -4663,7 +4663,7 @@ msgstr "Paramètres utilisés pour imprimer avec plusieurs extrudeuses." #: fdmprinter.def.json msgctxt "prime_tower_enable label" msgid "Enable Prime Tower" -msgstr "Activer la tour primaire" +msgstr "Activer la tour d'amorçage" #: fdmprinter.def.json msgctxt "prime_tower_enable description" @@ -4673,62 +4673,62 @@ msgstr "Imprimer une tour à côté de l'impression qui sert à amorcer le maté #: fdmprinter.def.json msgctxt "prime_tower_size label" msgid "Prime Tower Size" -msgstr "Taille de la tour primaire" +msgstr "Taille de la tour d'amorçage" #: fdmprinter.def.json msgctxt "prime_tower_size description" msgid "The width of the prime tower." -msgstr "La largeur de la tour primaire." +msgstr "La largeur de la tour d'amorçage." #: fdmprinter.def.json msgctxt "prime_tower_min_volume label" msgid "Prime Tower Minimum Volume" -msgstr "Volume minimum de la tour primaire" +msgstr "Volume minimum de la tour d'amorçage" #: fdmprinter.def.json msgctxt "prime_tower_min_volume description" msgid "The minimum volume for each layer of the prime tower in order to purge enough material." -msgstr "Le volume minimum pour chaque touche de la tour primaire afin de purger suffisamment de matériau." +msgstr "Le volume minimum pour chaque touche de la tour d'amorçage afin de purger suffisamment de matériau." #: fdmprinter.def.json msgctxt "prime_tower_position_x label" msgid "Prime Tower X Position" -msgstr "Position X de la tour primaire" +msgstr "Position X de la tour d'amorçage" #: fdmprinter.def.json msgctxt "prime_tower_position_x description" msgid "The x coordinate of the position of the prime tower." -msgstr "Les coordonnées X de la position de la tour primaire." +msgstr "Les coordonnées X de la position de la tour d'amorçage." #: fdmprinter.def.json msgctxt "prime_tower_position_y label" msgid "Prime Tower Y Position" -msgstr "Position Y de la tour primaire" +msgstr "Position Y de la tour d'amorçage" #: fdmprinter.def.json msgctxt "prime_tower_position_y description" msgid "The y coordinate of the position of the prime tower." -msgstr "Les coordonnées Y de la position de la tour primaire." +msgstr "Les coordonnées Y de la position de la tour d'amorçage." #: fdmprinter.def.json msgctxt "prime_tower_wipe_enabled label" msgid "Wipe Inactive Nozzle on Prime Tower" -msgstr "Essuyer le bec d'impression inactif sur la tour primaire" +msgstr "Essuyer le bec d'impression inactif sur la tour d'amorçage" #: fdmprinter.def.json msgctxt "prime_tower_wipe_enabled description" msgid "After printing the prime tower with one nozzle, wipe the oozed material from the other nozzle off on the prime tower." -msgstr "Après l'impression de la tour primaire à l'aide d'une buse, nettoyer le matériau qui suinte de l'autre buse sur la tour primaire." +msgstr "Après l'impression de la tour d'amorçage à l'aide d'une buse, nettoyer le matériau qui suinte de l'autre buse sur la tour d'amorçage." #: fdmprinter.def.json msgctxt "prime_tower_brim_enable label" msgid "Prime Tower Brim" -msgstr "Bordure de la tour primaire" +msgstr "Bordure de la tour d'amorçage" #: fdmprinter.def.json msgctxt "prime_tower_brim_enable description" msgid "Prime-towers might need the extra adhesion afforded by a brim even if the model doesn't. Presently can't be used with the 'Raft' adhesion-type." -msgstr "Les tours primaires peuvent avoir besoin de l'adhérence supplémentaire d'une bordure, même si le modèle n'en a pas besoin. Ne peut actuellement pas être utilisé avec le type d'adhérence « Raft » (radeau)." +msgstr "Les tours d'amorçage peuvent avoir besoin de l'adhérence supplémentaire d'une bordure, même si le modèle n'en a pas besoin. Ne peut actuellement pas être utilisé avec le type d'adhérence « Raft » (radeau)." #: fdmprinter.def.json msgctxt "ooze_shield_enabled label" @@ -4793,7 +4793,7 @@ msgstr "La vitesse à laquelle le filament est rétracté pendant une rétractio #: fdmprinter.def.json msgctxt "switch_extruder_prime_speed label" msgid "Nozzle Switch Prime Speed" -msgstr "Vitesse primaire de changement de buse" +msgstr "Vitesse d'amorçage de changement de buse" #: fdmprinter.def.json msgctxt "switch_extruder_prime_speed description" @@ -6187,7 +6187,7 @@ msgstr "La distance de rétraction du filament afin qu'il ne suinte pas pendant #: fdmprinter.def.json msgctxt "wipe_retraction_extra_prime_amount label" msgid "Wipe Retraction Extra Prime Amount" -msgstr "Degré supplémentaire de rétraction d'essuyage primaire" +msgstr "Degré supplémentaire de rétraction d'essuyage d'amorçage" #: fdmprinter.def.json msgctxt "wipe_retraction_extra_prime_amount description" @@ -6632,11 +6632,11 @@ msgstr "Matrice de transformation à appliquer au modèle lors de son chargement #~ msgctxt "prime_tower_circular label" #~ msgid "Circular Prime Tower" -#~ msgstr "Tour primaire circulaire" +#~ msgstr "Tour d'amorçage circulaire" #~ msgctxt "prime_tower_circular description" #~ msgid "Make the prime tower as a circular shape." -#~ msgstr "Réaliser la tour primaire en forme circulaire." +#~ msgstr "Réaliser la tour d'amorçage en forme circulaire." #~ msgctxt "prime_tower_flow description" #~ msgid "Flow compensation: the amount of material extruded is multiplied by this value." @@ -6772,11 +6772,11 @@ msgstr "Matrice de transformation à appliquer au modèle lors de son chargement #~ msgctxt "prime_tower_wall_thickness label" #~ msgid "Prime Tower Thickness" -#~ msgstr "Épaisseur de la tour primaire" +#~ msgstr "Épaisseur de la tour d'amorçage" #~ msgctxt "prime_tower_wall_thickness description" #~ msgid "The thickness of the hollow prime tower. A thickness larger than half the Prime Tower Minimum Volume will result in a dense prime tower." -#~ msgstr "L'épaisseur de la tour primaire creuse. Une épaisseur supérieure à la moitié du volume minimum de la tour primaire résultera en une tour primaire dense." +#~ msgstr "L'épaisseur de la tour d'amorçage creuse. Une épaisseur supérieure à la moitié du volume minimum de la tour d'amorçage résultera en une tour d'amorçage dense." #~ msgctxt "dual_pre_wipe label" #~ msgid "Wipe Nozzle After Switch" @@ -6788,11 +6788,11 @@ msgstr "Matrice de transformation à appliquer au modèle lors de son chargement #~ msgctxt "prime_tower_purge_volume label" #~ msgid "Prime Tower Purge Volume" -#~ msgstr "Volume de purge de la tour primaire" +#~ msgstr "Volume de purge de la tour d'amorçage" #~ msgctxt "prime_tower_purge_volume description" #~ msgid "Amount of filament to be purged when wiping on the prime tower. Purging is useful for compensating the filament lost by oozing during inactivity of the nozzle." -#~ msgstr "Quantité de filament à purger lors de l'essuyage de la tour primaire. La purge est utile pour compenser le filament perdu par la suinte pendant l'inactivité de la buse." +#~ msgstr "Quantité de filament à purger lors de l'essuyage de la tour d'amorçage. La purge est utile pour compenser le filament perdu par la suinte pendant l'inactivité de la buse." #~ msgctxt "bridge_wall_max_overhang label" #~ msgid "Bridge Wall Max Overhang" diff --git a/resources/meshes/atmat_asterion_platform.stl b/resources/meshes/atmat_asterion_platform.stl new file mode 100644 index 0000000000..de7f384997 Binary files /dev/null and b/resources/meshes/atmat_asterion_platform.stl differ diff --git a/resources/meshes/atmat_signal_pro_platform.stl b/resources/meshes/atmat_signal_pro_platform.stl new file mode 100644 index 0000000000..697a7870dc Binary files /dev/null and b/resources/meshes/atmat_signal_pro_platform.stl differ diff --git a/resources/meshes/dagoma_discoeasy200.3mf b/resources/meshes/dagoma_discoeasy200.3mf index 9c2c674cfc..d71b5da20a 100644 Binary files a/resources/meshes/dagoma_discoeasy200.3mf and b/resources/meshes/dagoma_discoeasy200.3mf differ diff --git a/resources/meshes/dagoma_discoeasy200_bicolor.3mf b/resources/meshes/dagoma_discoeasy200_bicolor.3mf new file mode 100644 index 0000000000..9c2c674cfc Binary files /dev/null and b/resources/meshes/dagoma_discoeasy200_bicolor.3mf differ diff --git a/resources/meshes/dagoma_discoultimate.3mf b/resources/meshes/dagoma_discoultimate.3mf index 71b1e4dfba..f68fe2677c 100644 Binary files a/resources/meshes/dagoma_discoultimate.3mf and b/resources/meshes/dagoma_discoultimate.3mf differ diff --git a/resources/meshes/dagoma_discoultimate_bicolor.3mf b/resources/meshes/dagoma_discoultimate_bicolor.3mf new file mode 100644 index 0000000000..71b1e4dfba Binary files /dev/null and b/resources/meshes/dagoma_discoultimate_bicolor.3mf differ diff --git a/resources/meshes/tinyboy_ra20.obj b/resources/meshes/tinyboy_ra20.obj new file mode 100644 index 0000000000..a557848931 --- /dev/null +++ b/resources/meshes/tinyboy_ra20.obj @@ -0,0 +1,9231 @@ +# Object Export From Tinkercad Server 2015 + +mtllib obj.mtl + +o obj_0 +v -97 85.95 348.873 +v -89.825 85.657 51.415 +v -70.775 -70.966 72.962 +v -97 72.003 295.948 +v -97 71.961 296.886 +v -70.796 -70.976 72.955 +v -70.886 -70.697 72.962 +v -70.908 -70.704 72.955 +v 52.594 -71.568 72.981 +v -97 71.837 297.802 +v 85.402 -84.102 49.992 +v -97 71.635 298.672 +v -97 71.358 299.475 +v -97 71.014 300.191 +v -97 70.611 300.803 +v -97 70.16 301.295 +v 52.362 -71.623 72.981 +v -97 69.67 301.656 +v -97 69.155 301.875 +v -97 68.627 301.949 +v -70.714 -70.935 72.969 +v 52.727 -71.306 72.987 +v -89.402 85.689 51.415 +v -70.821 -70.676 72.969 +v 52.535 -71.386 72.987 +v 52.332 -71.435 72.987 +v -97 -86.95 348.873 +v -97 -62.961 296.886 +v -97 -63.002 295.948 +v -97 -62.837 297.802 +v -97 -62.635 298.672 +v -97 -62.358 299.475 +v -97 -62.014 300.191 +v -97 -61.611 300.803 +v -97 -61.16 301.295 +v -97 -60.67 301.656 +v -97 -60.155 301.875 +v -97 -59.627 301.949 +v 52.466 -71.173 72.991 +v -69.704 -71.908 72.955 +v 52.297 -71.214 72.991 +v -69.976 -71.796 72.955 +v 85.985 -84.507 50.084 +v -69.697 -71.886 72.962 +v 85.911 -84.589 50.084 +v -97 71.837 66.03 +v -97 71.961 66.946 +v -97 72.003 67.884 +v -69.966 -71.775 72.962 +v 85.825 -84.659 50.084 +v -97 68.627 61.883 +v -97 69.155 61.957 +v -97 69.67 62.176 +v -97 70.16 62.537 +v -97 70.611 63.029 +v -97 71.014 63.641 +v -97 71.358 64.357 +v -97 71.635 65.16 +v -69.676 -71.821 72.969 +v 85.729 -84.716 50.084 +v -69.935 -71.714 72.969 +v 85.625 -84.757 50.084 +v -97 85.95 2.123 +v 52.125 -71.642 72.981 +v -97 -62.837 66.03 +v -97 -62.961 66.946 +v 52.125 -71.451 72.987 +v -97 -63.002 67.884 +v -69.641 -71.714 72.975 +v -97 -60.155 61.957 +v -97 -59.627 61.883 +v -97 -60.67 62.176 +v 52.125 -71.227 72.991 +v -97 -61.16 62.537 +v -97 -61.611 63.029 +v -97 -62.014 63.641 +v -97 -62.358 64.357 +v 54 51.125 72.955 +v -97 -62.635 65.16 +v 52.125 -70.976 72.995 +v 54 51.125 70.045 +v 52.216 -70.697 72.998 +v 52.125 -70.704 72.998 +v -97 -86.95 2.123 +v 52.976 -71.796 72.955 +v -69.704 -70.125 70.002 +v -92.25 -91.5 348.873 +v 52.966 -71.775 72.962 +v -74.003 -91.5 295.948 +v -73.958 -91.5 296.886 +v -69.976 -70.125 70.005 +v -73.826 -91.5 297.802 +v 52.935 -71.714 72.969 +v -73.61 -91.5 298.672 +v -73.315 -91.5 299.475 +v -72.948 -91.5 300.191 +v -72.518 -91.5 300.803 +v -70.227 -70.125 70.009 +v 85.515 -84.782 50.084 +v 52.704 -71.908 72.955 +v -70.451 -70.125 70.013 +v 52.418 -71.977 72.955 +v -89.985 86.618 53.064 +v -89.911 86.175 52.175 +v -70.642 -70.125 70.019 +v 52.697 -71.886 72.962 +v 85.402 -84.791 50.084 +v 52.415 -71.954 72.962 +v -69.641 -70.388 70.002 +v -89.402 86.214 52.175 +v 54 -70.125 72.955 +v -69.676 -70.304 70.002 +v 52.676 -71.821 72.969 +v 54 -70.125 70.045 +v 52.404 -71.886 72.969 +v -69.697 -70.216 70.002 +v -89.402 86.662 53.064 +v 85.402 -88.287 55.14 +v 85.402 -88.023 54.06 +v 52.641 -71.714 72.975 +v -69.814 -70.625 70.005 +v -90.044 86.975 54.06 +v 52.386 -71.775 72.975 +v -69.883 -70.511 70.005 +v 85.402 -88.448 56.277 +v -69.935 -70.388 70.005 +v -90.087 87.236 55.14 +v -89.402 87.287 55.14 +v -69.966 -70.258 70.005 +v -89.402 87.023 54.06 +v 52.125 -72 72.955 +v 52.125 -71.977 72.962 +v 52.125 53 72.955 +v 52.125 -71.908 72.969 +v 52.125 53 70.045 +v -89.402 87.448 56.277 +v 52.125 -71.796 72.975 +v 51.48 2.655 350.995 +v 52.141 0.806 350.995 +v -70.107 -70.625 70.009 +v 52.418 -70.125 70.001 +v 51.48 2.655 347.998 +v 86.63 -86.407 51.415 +v -72.037 -91.5 301.295 +v 52.704 -70.125 70.002 +v -71.515 -91.5 301.656 +v 52.976 -70.125 70.005 +v -73.826 -91.5 66.03 +v -73.958 -91.5 66.946 +v 87.702 -86.302 52.175 +v 53.227 -70.125 70.009 +v -74.003 -91.5 67.884 +v -71.515 -91.5 62.176 +v 87.314 -86.619 52.175 +v -72.037 -91.5 62.537 +v -70.173 -70.466 70.009 +v 86.992 -86.195 51.415 +v -72.518 -91.5 63.029 +v -72.948 -91.5 63.641 +v -94.002 83.102 57.443 +v -73.315 -91.5 64.357 +v -70.214 -70.297 70.009 +v -93.946 83.791 57.443 +v -73.61 -91.5 65.16 +v -93.777 84.462 57.443 +v 52.966 -70.258 70.005 +v -93.501 85.1 57.443 +v 52.141 0.806 347.998 +v 52.935 -70.388 70.005 +v -93.124 85.689 57.443 +v 86.408 -87.061 52.175 +v 86.238 -86.562 51.415 +v 52.883 -70.511 70.005 +v 52.814 -70.625 70.005 +v -92.655 86.214 57.443 +v 52.618 -1.098 350.995 +v 52.906 -3.039 350.995 +v 53.003 -5 350.995 +v 53.214 -70.297 70.009 +v 52.906 -6.961 350.995 +v 52.618 -8.902 350.995 +v 52.141 -10.806 350.995 +v 51.48 -12.655 350.995 +v 53.173 -70.466 70.009 +v -70.386 -70.535 70.013 +v 86.879 -86.875 52.175 +v 53.107 -70.625 70.009 +v -92.106 86.662 57.443 +v -70.435 -70.332 70.013 +v -91.491 87.023 57.443 +v -70.568 -70.594 70.019 +v 52.618 -1.098 347.998 +v -70.623 -70.362 70.019 +v -92.25 -91.5 2.123 +v -70.714 -70.641 70.025 +v -92.25 90.5 348.873 +v -70.775 -70.386 70.025 +v 88.034 -86.619 53.064 +v -70.796 -70.125 70.025 +v -73.958 90.5 296.886 +v -74.003 90.5 295.948 +v -73.826 90.5 297.802 +v -70.886 -70.404 70.031 +v 52.906 -3.039 347.998 +v 52.697 -70.216 70.002 +v -70.908 -70.125 70.031 +v 53.003 -5 347.998 +v 87.092 -87.274 53.064 +v 52.676 -70.304 70.002 +v 52.906 -6.961 347.998 +v -70.954 -70.415 70.038 +v 52.618 -8.902 347.998 +v 52.641 -70.388 70.002 +v -70.977 -70.125 70.038 +v 52.141 -10.806 347.998 +v -70.977 -70.418 70.045 +v 51.48 -12.655 347.998 +v -71 -70.125 70.045 +v -73.61 90.5 298.672 +v 87.59 -86.982 53.064 +v -73.315 90.5 299.475 +v -72.948 90.5 300.191 +v -72.518 90.5 300.803 +v -69.727 -70.727 70.005 +v -72.037 90.5 301.295 +v -71.515 90.5 301.656 +v 52.415 -70.171 70.001 +v -69.773 -71.017 70.009 +v 52.404 -70.216 70.001 +v 52.386 -70.258 70.001 +v -69.904 -70.904 70.009 +v 52.362 -70.297 70.001 +v 52.332 -70.332 70.001 +v 52.297 -70.362 70.001 +v -70.017 -70.773 70.009 +v 52.258 -70.386 70.001 +v -73.826 90.5 66.03 +v 86.552 -87.488 53.064 +v -73.958 90.5 66.946 +v -90.824 87.287 57.443 +v -74.003 90.5 67.884 +v -70.063 -71.063 70.013 +v -90.122 87.448 57.443 +v 52.125 51.125 73 +v -70.198 -70.904 70.013 +v -70.306 -70.727 70.013 +v -70.352 -71.017 70.019 +v -70.477 -70.814 70.019 +v -70.477 -71.107 70.025 +v 52.594 -70.466 70.002 +v -70.614 -70.883 70.025 +v 52.535 -70.535 70.002 +v 52.466 -70.594 70.002 +v -71.515 90.5 62.176 +v -72.037 90.5 62.537 +v -72.518 90.5 63.029 +v -72.948 90.5 63.641 +v -73.315 90.5 64.357 +v -73.61 90.5 65.16 +v -89.402 87.503 57.443 +v 52.388 -70.641 70.002 +v -69.727 -71.306 70.013 +v -69.904 -71.198 70.013 +v -92.25 90.5 2.123 +v -69.814 -71.477 70.019 +v -70.017 -71.352 70.019 +v -69.883 -71.614 70.025 +v -70.107 -71.477 70.025 +v 52.125 -70.125 70 +v -89.402 83.102 49.992 +v -70.198 -71.198 70.019 +v -70.306 -71.306 70.025 +v 52.216 -70.404 70.001 +v 52.171 -70.415 70.001 +v -70.386 -71.386 70.031 +v 52.125 -70.418 70.001 +v -70.568 -71.173 70.031 +v -90.087 83.315 347.906 +v -70.623 -71.214 70.038 +v 88.413 -86.195 53.064 +v -94.002 68.627 61.883 +v 53.017 -70.773 70.009 +v -70.642 -71.227 70.045 +v 52.904 -70.904 70.009 +v 86.238 -85.202 50.357 +v 52.773 -71.017 70.009 +v -70.173 -71.568 70.031 +v 53.198 -70.904 70.013 +v -70.214 -71.623 70.038 +v -70.435 -71.435 70.038 +v 53.063 -71.063 70.013 +v -70.227 -71.642 70.045 +v 86.879 -85.515 50.805 +v 86.408 -85.064 50.357 +v -92.993 90.444 2.123 +v -93.718 90.277 2.123 +v -70.451 -71.451 70.045 +v -90.044 83.415 347.906 +v -92.25 90.444 0.696 +v -92.984 90.389 0.696 +v 86.63 -85.718 50.805 +v -93.7 90.224 0.696 +v -89.402 83.102 347.998 +v 86.048 -85.314 50.357 +v -70.714 -70.935 70.031 +v -92.25 90.3607 0.001 +v 52.304 -70.676 70.002 +v -92.6027 90.3342 0.001 +v -70.821 -70.676 70.031 +v -92.9705 90.3067 0.001 +v 52.727 -70.727 70.005 +v -93.3143 90.2278 0.001 +v -93.6731 90.1452 0.001 +v -89.985 83.507 347.906 +v -70.775 -70.966 70.038 +v 52.625 -70.814 70.005 +v -89.911 83.589 347.906 +v -70.886 -70.697 70.038 +v 52.511 -70.883 70.005 +v -89.825 83.659 347.906 +v 86.35 -85.882 50.805 +v 52.388 -70.935 70.005 +v -89.729 83.716 347.906 +v -70.796 -70.976 70.045 +v 52.258 -70.966 70.005 +v -89.625 83.757 347.906 +v 86.048 -86.002 50.805 +v -70.908 -70.704 70.045 +v -93.9999 90.0154 0.001 +v -89.515 83.782 347.906 +v -94.406 90.004 2.123 +v -89.402 83.791 347.906 +v -95.042 89.631 2.123 +v 52.625 -71.107 70.009 +v -95.609 89.167 2.123 +v -96.093 88.624 2.123 +v -96.482 88.016 2.123 +v -69.641 -71.714 70.025 +v -96.768 87.356 2.123 +v 87.263 -85.009 50.805 +v -96.942 86.662 2.123 +v -69.676 -71.821 70.031 +v -69.935 -71.714 70.031 +v -69.697 -71.886 70.038 +v -69.966 -71.775 70.038 +v 87.092 -85.277 50.805 +v -69.704 -71.908 70.045 +v -69.976 -71.796 70.045 +v 52.904 -71.198 70.013 +v 53.198 -71.198 70.019 +v -94.38 89.954 0.696 +v -90.048 84.314 347.633 +v 86.122 -84.102 50.084 +v 53.017 -71.352 70.019 +v -95.008 89.586 0.696 +v -95.567 89.128 0.696 +v -89.625 84.445 347.633 +v 52.125 -70.125 73 +v 86.113 -84.21 50.084 +v -96.046 88.592 0.696 +v -89.402 84.462 347.633 +v 52.814 -71.477 70.019 +v 86.087 -84.315 50.084 +v 53.107 -71.477 70.025 +v -94.3406 89.8802 0.001 +v -94.6419 89.703 0.001 +v 86.044 -84.415 50.084 +v 52.883 -71.614 70.025 +v -94.9566 89.5186 0.001 +v -89.842 84.396 347.633 +v -95.5056 89.0691 0.001 +v -95.2255 89.2986 0.001 +v 61.535 -94.997 34.116 +v 62.167 -94.997 33.738 +v -95.7356 88.8117 0.001 +v -95.9757 88.5431 0.001 +v -96.1603 88.2542 0.001 +v 62.758 -94.997 33.299 +v 53.173 -71.568 70.031 +v -96.43 87.99 0.696 +v 63.303 -94.997 32.805 +v 53.451 52.451 72.955 +v -96.712 87.339 0.696 +v 86.824 -84.102 50.357 +v 53.642 52.227 72.955 +v 86.806 -84.315 50.357 +v 53.435 52.435 72.962 +v 86.754 -84.523 50.357 +v -96.884 86.653 0.696 +v 53.214 -71.623 70.038 +v -96.3527 87.9526 0.001 +v -89.402 85.689 346.575 +v -96.4879 87.6393 0.001 +v 86.552 -84.902 50.357 +v -96.6292 87.3131 0.001 +v -96.712 86.9838 0.001 +v -96.7982 86.64 0.001 +v 53.227 -71.642 70.045 +v -96.8261 86.3023 0.001 +v 53.386 52.386 72.969 +v 52.466 -71.173 70.009 +v -96.942 85.95 0.696 +v 52.297 -71.214 70.009 +v -96.8552 85.95 0.001 +v 53.306 52.306 72.975 +v 52.727 -71.306 70.013 +v 52.535 -71.386 70.013 +v 53.796 51.976 72.955 +v 58 -94.997 35.002 +v 52.332 -71.435 70.013 +v 53.775 51.966 72.962 +v 86.669 -84.72 50.357 +v 58.735 -94.997 34.965 +v 59.463 -94.997 34.857 +v 53.623 52.214 72.962 +v 52.594 -71.568 70.019 +v 60.177 -94.997 34.679 +v 52.362 -71.623 70.019 +v 87.491 -84.102 50.805 +v 87.465 -84.415 50.805 +v 53.568 52.173 72.969 +v 53.714 51.935 72.969 +v 53.477 52.107 72.975 +v -94.002 -62.837 66.03 +v -94.002 -62.961 66.946 +v 53.614 51.883 72.975 +v -94.002 -63.002 67.884 +v -73.826 -88.502 66.03 +v 52.216 -70.697 70.002 +v -73.958 -88.502 66.946 +v 53.352 52.017 72.981 +v 87.389 -84.72 50.805 +v -74.003 -88.502 67.884 +v 52.125 -70.704 70.002 +v 60.87 -94.997 34.431 +v 53.477 51.814 72.981 +v 52.125 -70.976 70.005 +v 53.306 51.727 72.987 +v 52.125 -71.227 70.009 +v 52.125 -71.451 70.013 +v -90.048 85.002 347.186 +v 87.974 -84.902 51.415 +v 52.125 -71.642 70.019 +v -89.825 85.657 346.575 +v 52.935 -71.714 70.031 +v 87.314 -85.931 51.415 +v -94.002 -60.155 61.957 +v -94.002 -59.627 61.883 +v -94.002 -60.67 62.176 +v -94.002 -61.16 62.537 +v -94.002 -61.611 63.029 +v -89.729 85.075 347.186 +v -94.002 -62.014 63.641 +v -94.002 -62.358 64.357 +v 52.966 -71.775 70.038 +v -94.002 -62.635 65.16 +v 87.812 -85.277 51.415 +v -89.402 85.1 347.186 +v 88.034 -85.931 52.175 +v 52.976 -71.796 70.045 +v 87.59 -85.623 51.415 +v 53.833 -94.997 33.738 +v 53.908 51.704 72.955 +v 54.465 -94.997 34.116 +v 53.977 51.418 72.955 +v 55.13 -94.997 34.431 +v 53.886 51.697 72.962 +v 52.641 -71.714 70.025 +v 52.386 -71.775 70.025 +v 53.954 51.415 72.962 +v 55.823 -94.997 34.679 +v 52.676 -71.821 70.031 +v 88.106 -84.102 51.415 +v 53.821 51.676 72.969 +v 52.404 -71.886 70.031 +v 56.537 -94.997 34.857 +v 53.886 51.404 72.969 +v 88.655 -84.102 52.175 +v 88.073 -84.507 51.415 +v 52.697 -71.886 70.038 +v 88.496 -85.064 52.175 +v 57.265 -94.997 34.965 +v 52.415 -71.954 70.038 +v 53.775 51.386 72.975 +v 52.704 -71.908 70.045 +v -71.515 -88.502 62.176 +v 52.418 -71.977 70.045 +v -72.037 -88.502 62.537 +v -72.518 -88.502 63.029 +v 53.714 51.641 72.975 +v -72.948 -88.502 63.641 +v 88.615 -84.589 52.175 +v -73.315 -88.502 64.357 +v 53.568 51.594 72.981 +v -73.61 -88.502 65.16 +v 53.623 51.362 72.981 +v 52.125 -71.796 70.025 +v 52.125 -71.908 70.031 +v 52.697 -94.997 32.805 +v 88.301 -85.515 52.175 +v 53.242 -94.997 33.299 +v 52.125 -71.977 70.038 +v 52.125 -72 70.045 +v 53.386 51.535 72.987 +v 53.435 51.332 72.987 +v 89.124 -84.102 53.064 +v 89.078 -84.659 53.064 +v -92.413 -86.195 53.064 +v -89.985 86.618 344.926 +v -89.402 86.662 344.926 +v 64.344 -92.497 35.232 +v 64.344 -92 35.232 +v 88.942 -85.202 53.064 +v 63.556 -92 35.816 +v 63.556 -92.497 35.816 +v -89.911 86.175 345.815 +v 62.714 -92 36.321 +v 62.714 -92.497 36.321 +v 61.827 -92 36.74 +v 61.827 -92.497 36.74 +v -90.238 -86.562 51.415 +v 88.718 -85.718 53.064 +v -89.402 86.214 345.815 +v -90.63 -86.407 51.415 +v 63.798 -94.997 32.259 +v 64.236 -94.997 31.668 +v 64.614 -94.997 31.037 +v 60.903 -92 37.071 +v 60.903 -92.497 37.071 +v 59.951 -92 37.309 +v 59.951 -92.497 37.309 +v 58.98 -92 37.453 +v 58.98 -92.497 37.453 +v 58 -92 37.502 +v 58 -92.497 37.502 +v 53.306 52.306 70.025 +v 87.812 -87.274 54.06 +v 53.386 52.386 70.031 +v -92.25 90.444 350.3 +v -92.25 90.3606 350.995 +v -92.984 90.389 350.3 +v -92.9705 90.3066 350.995 +v -92.6175 90.3331 350.995 +v 64.929 -94.997 30.372 +v -90.408 -87.061 52.175 +v -93.7 90.224 350.3 +v -93.329 90.224 350.995 +v 86.044 -87.975 54.06 +v -93.673 90.1451 350.995 +v 65.177 -94.997 29.679 +v -91.314 -86.619 52.175 +v -90.992 -86.195 51.415 +v -92.993 90.444 348.873 +v -93.718 90.277 348.873 +v 87.974 -87.488 55.14 +v 57.02 -92 37.453 +v 57.02 -92.497 37.453 +v 56.049 -92 37.309 +v 56.049 -92.497 37.309 +v 55.097 -92 37.071 +v 55.097 -92.497 37.071 +v 87.389 -87.831 55.14 +v 87.263 -87.596 54.06 +v 65.356 -94.997 28.965 +v 54.173 -92 36.74 +v 54.173 -92.497 36.74 +v 86.754 -88.082 55.14 +v 53.286 -92 36.321 +v 53.286 -92.497 36.321 +v -94.0135 90.0099 350.995 +v 86.669 -87.831 54.06 +v -90.879 -86.875 52.175 +v -94.38 89.954 350.3 +v -94.3406 89.8801 350.995 +v 52.444 -92 35.816 +v 52.444 -92.497 35.816 +v -94.9566 89.5186 350.995 +v -91.702 -86.302 52.175 +v -95.008 89.586 350.3 +v 51.656 -92 35.232 +v 51.656 -92.497 35.232 +v -94.655 89.6958 350.995 +v -95.567 89.128 350.3 +v -95.2365 89.2893 350.995 +v -95.5056 89.0691 350.995 +v -95.9756 88.5431 350.995 +v -96.046 88.592 350.3 +v -90.552 -87.488 53.064 +v 86.087 -88.236 55.14 +v -95.7454 88.8007 350.995 +v 65.464 -94.997 28.237 +v 65.5 -94.997 27.502 +v -96.1679 88.2416 350.995 +v -91.092 -87.274 53.064 +v 65.464 -94.997 26.766 +v 65.356 -94.997 26.038 +v 65.177 -94.997 25.324 +v -92.034 -86.619 53.064 +v 64.929 -94.997 24.631 +v 88.942 -86.562 55.14 +v 88.718 -86.407 54.06 +v -91.59 -86.982 53.064 +v 88.301 -86.875 54.06 +v -96.43 87.99 350.3 +v -96.3526 87.9526 350.995 +v -96.712 87.339 350.3 +v -96.6291 87.313 350.995 +v -96.4938 87.6266 350.995 +v -96.884 86.653 350.3 +v -96.7152 86.9695 350.995 +v 88.496 -87.061 55.14 +v -92.655 -84.102 52.175 +v -96.7981 86.64 350.995 +v -96.8272 86.288 350.995 +v -92.496 -85.064 52.175 +v 64.614 -94.997 23.966 +v 64.236 -94.997 23.335 +v 63.798 -94.997 22.744 +v -92.718 -85.718 53.064 +v -92.301 -85.515 52.175 +v -94.406 90.004 348.873 +v 87.465 -87.975 56.277 +v -92.942 -85.202 53.064 +v -95.042 89.631 348.873 +v -95.609 89.167 348.873 +v 53.435 52.435 70.038 +v 86.806 -88.236 56.277 +v -96.093 88.624 348.873 +v 63.303 -94.997 22.198 +v 62.758 -94.997 21.704 +v 86.113 -88.395 56.277 +v 53.451 52.451 70.045 +v 62.167 -94.997 21.266 +v 53.642 52.227 70.045 +v 61.535 -94.997 20.887 +v -96.482 88.016 348.873 +v -96.768 87.356 348.873 +v 65.73 -92 33.845 +v 65.73 -92.497 33.845 +v 89.078 -86.657 56.277 +v -96.942 86.662 348.873 +v 88.615 -87.175 56.277 +v 53.775 51.966 70.038 +v 53.796 51.976 70.045 +v 89.451 -86.075 56.277 +v -96.942 85.95 350.3 +v -96.8551 85.95 350.995 +v 62.9886 -93.5699 33.09 +v 63.303 -93.5796 32.805 +v 60.87 -94.997 20.572 +v 53.352 52.017 70.019 +v 53.477 52.107 70.025 +v 88.073 -87.618 56.277 +v -93.124 83.102 344.926 +v -93.078 83.659 344.926 +v -93.078 -84.659 53.064 +v 62.758 -93.5624 33.299 +v -92.615 -84.589 52.175 +v 89.501 -84.102 54.06 +v -92.655 83.102 345.815 +v -92.615 83.589 345.815 +v 89.301 -85.314 54.06 +v 89.054 -85.882 54.06 +v 61.535 -93.5334 34.116 +v 61.883 -93.5405 33.9078 +v 62.167 -93.5466 33.738 +v 62.4585 -93.5544 33.5215 +v 89.777 -84.102 55.14 +v 89.723 -84.757 55.14 +v 53.568 52.173 70.031 +v 89.451 -84.716 54.06 +v 58 -94.997 20.002 +v 53.623 52.214 70.038 +v 60.177 -94.997 20.324 +v 59.463 -94.997 20.146 +v 65.071 -92 34.573 +v 65.071 -92.497 34.573 +v 58.735 -94.997 20.038 +v -93.124 -84.102 53.064 +v -92.106 83.102 346.575 +v 53.306 51.727 70.013 +v 63.803 -92 32.8082 +v -91.974 83.902 346.575 +v -90.238 -85.202 50.357 +v 53.477 51.814 70.019 +v 63.803 -91.5 32.7689 +v 63.8038 -91.5 32.768 +v -91.491 83.102 347.186 +v -92.073 83.507 346.575 +v 63.258 -92 33.2794 +v 63.258 -91.5 33.2401 +v 89.301 -86.002 55.14 +v 53.614 51.883 70.025 +v 62.667 -92 33.6966 +v 62.667 -91.5 33.6573 +v 62.035 -92 34.0576 +v 62.035 -91.5 34.0183 +v 51.071 -94.997 30.372 +v 53.714 51.935 70.031 +v 61.44 -91.5 34.2866 +v 61.8946 -91.5 34.0816 +v 51.386 -94.997 31.037 +v 89.563 -85.396 55.14 +v -91.465 83.415 347.186 +v 51.764 -94.997 31.668 +v 61.2678 -93.5292 34.2426 +v 52.202 -94.997 32.259 +v -91.389 83.72 347.186 +v 60.6163 -93.5196 34.5218 +v 60.87 -93.5227 34.431 +v 89.946 -84.102 56.277 +v 53.775 51.386 70.025 +v 89.89 -84.782 56.277 +v 53.821 51.676 70.031 +v 59.9332 -93.512 34.7398 +v 89.723 -85.445 56.277 +v 60.177 -93.5141 34.679 +v 53.886 51.404 70.031 +v -90.824 83.102 347.633 +v 51.071 -94.997 24.631 +v -90.806 83.315 347.633 +v 58.5 -93.5035 34.9768 +v 58.735 -93.5038 34.965 +v 59.2254 -93.5062 34.8923 +v 59.463 -93.5076 34.857 +v -90.754 83.523 347.633 +v 58.0634 -93.5026 34.9988 +v 57.765 -93.503 34.985 +v 55.13 -94.997 20.572 +v -90.669 83.72 347.633 +v 53.886 51.697 70.038 +v 54.465 -94.997 20.887 +v 53.954 51.415 70.038 +v 85.402 -88.502 57.443 +v 53.833 -94.997 21.266 +v -90.552 83.902 347.633 +v -90.35 -85.882 50.805 +v 53.908 51.704 70.045 +v -90.122 83.102 347.906 +v 53.977 51.418 70.045 +v -90.63 -85.718 50.805 +v -90.113 83.21 347.906 +v 61.37 -92 34.3574 +v 61.37 -91.5 34.3181 +v -90.408 -85.064 50.357 +v 60.677 -92 34.5931 +v 60.677 -91.5 34.5537 +v 59.963 -92 34.7635 +v 59.963 -91.5 34.7242 +v 60.2263 -91.5 34.6614 +v -91.092 -85.277 50.805 +v 60.5437 -91.5 34.5856 +v 53.386 51.535 70.013 +v 58.735 -92 34.965 +v -91.263 -85.009 50.805 +v 53.435 51.332 70.013 +v 59.463 -92.2864 34.857 +v 59.235 -92.3051 34.8908 +v 59.463 -92 34.857 +v 59.6727 -92 34.8047 +v 59.235 -91.5 34.8275 +v 89.501 -86.1 57.443 +v 53.568 51.594 70.019 +v 58 -92 35.002 +v 57.265 -94.997 20.038 +v 89.124 -86.689 57.443 +v 56.537 -94.997 20.146 +v 58.735 -92.9537 34.965 +v 58.5 -92.9656 34.9768 +v 53.623 51.362 70.019 +v 55.823 -94.997 20.324 +v 58.5 -91.5 34.8616 +v -92.718 84.719 344.926 +v 57.765 -91.5 34.8275 +v 53.714 51.641 70.025 +v -90.879 -85.515 50.805 +v 57.344 -91.5 34.7678 +v 53.977 51.125 72.962 +v -92.496 84.064 345.815 +v -92.942 84.202 344.926 +v 53.908 51.125 72.969 +v -92.301 84.515 345.815 +v 53.796 51.125 72.975 +v 53.642 51.125 72.981 +v -92.413 85.195 344.926 +v 88.655 -87.214 57.443 +v 57.037 -93.507 34.882 +v 53.451 51.125 72.987 +v 88.106 -87.662 57.443 +v 53.242 -94.997 21.704 +v 87.491 -88.023 57.443 +v 52.697 -94.997 22.198 +v 53.451 51.125 70.013 +v 86.824 -88.287 57.443 +v 52.202 -94.997 22.744 +v 53.642 51.125 70.019 +v 86.122 -88.448 57.443 +v 53.796 51.125 70.025 +v 53.908 51.125 70.031 +v -91.59 85.982 344.926 +v 53.742 -93.559 33.402 +v 53.977 51.125 70.038 +v -92.034 84.931 345.815 +v 51.764 -94.997 23.335 +v 52.976 52.796 72.955 +v 90.002 -84.102 57.443 +v 89.946 -84.791 57.443 +v -92.034 85.62 344.926 +v 52.966 52.775 72.962 +v 89.777 -85.462 57.443 +v 51.386 -94.997 23.966 +v 56.323 -93.513 34.712 +v -91.314 85.62 345.815 +v 54.333 -93.544 33.818 +v 53.227 52.642 72.955 +v 53.214 52.623 72.962 +v 52.935 52.714 72.969 +v 53.173 52.568 72.969 +v -91.702 85.302 345.815 +v 61.827 -92 18.263 +v 61.827 -92.497 18.263 +v 62.714 -92 18.682 +v 62.714 -92.497 18.682 +v 63.556 -92 19.187 +v 63.556 -92.497 19.187 +v 64.344 -92 19.771 +v 64.344 -92.497 19.771 +v 52.883 52.614 72.975 +v 53.197 -93.575 32.932 +v -91.465 -84.415 50.805 +v 58 -92 20.002 +v 58 -92 17.502 +v 58.98 -92 17.55 +v 59.951 -92 17.694 +v 60.903 -92 17.932 +v 66.402 -88.502 61.883 +v -91.389 -84.72 50.805 +v -90.879 85.875 345.815 +v 59.951 -92.497 17.694 +v 53.107 52.477 72.975 +v 60.903 -92.497 17.932 +v 52.814 52.477 72.981 +v 93 71.837 66.03 +v 58.98 -92.497 17.55 +v -91.491 -84.102 50.805 +v 58 -92.497 17.502 +v 93 71.961 66.946 +v 53.017 52.352 72.981 +v 93 72.003 67.884 +v 93 69.155 61.957 +v 93 68.627 61.883 +v 93 69.67 62.176 +v 93 70.16 62.537 +v 55.63 -93.521 34.477 +v 93 70.611 63.029 +v 93 71.014 63.641 +v 93 71.358 64.357 +v -90.122 -84.102 50.084 +v 54.965 -93.531 34.178 +v 93 71.635 65.16 +v 53.286 -92 18.682 +v 54.173 -92 18.263 +v -90.113 -84.21 50.084 +v 55.097 -92 17.932 +v 56.049 -92 17.694 +v 57.02 -92 17.55 +v 56.049 -92.497 17.694 +v 57.02 -92.497 17.55 +v -90.552 -84.902 50.357 +v 52.418 52.977 72.955 +v 52.704 52.908 72.955 +v 52.415 52.954 72.962 +v 53.286 -92.497 18.682 +v 54.173 -92.497 18.263 +v 55.097 -92.497 17.932 +v 52.697 52.886 72.962 +v -90.754 -84.523 50.357 +v -91.59 84.623 346.575 +v 56.537 -92 34.857 +v 57.265 -92 34.965 +v -91.314 84.931 346.575 +v 52.404 52.886 72.969 +v 51.656 -92 19.771 +v 51.656 -92.497 19.771 +v 55.823 -92 34.679 +v 52.444 -92 19.187 +v 52.444 -92.497 19.187 +v 52.676 52.821 72.969 +v -90.669 -84.72 50.357 +v 93 -62.837 66.03 +v 93 -62.961 66.946 +v 93 -63.002 67.884 +v 93 -59.627 61.883 +v 93 -60.155 61.957 +v 88.25 -91.5 2.123 +v 93 -60.67 62.176 +v 93 -61.16 62.537 +v 52.386 52.775 72.975 +v 93 -61.611 63.029 +v 93 -62.014 63.641 +v 93 -62.358 64.357 +v 93 -62.635 65.16 +v 57.037 -91.5 34.7242 +v -90.806 -84.315 50.357 +v 69.826 90.5 66.03 +v 69.958 90.5 66.946 +v 70.003 90.5 67.884 +v 66.402 90.5 61.883 +v 66.966 90.5 61.957 +v 67.515 90.5 62.176 +v -90.824 -84.102 50.357 +v 68.037 90.5 62.537 +v 55.13 -92 34.431 +v 68.518 90.5 63.029 +v 68.948 90.5 63.641 +v 69.315 90.5 64.357 +v 52.641 52.714 72.975 +v 69.61 90.5 65.16 +v -91.263 84.009 347.186 +v -92.106 -84.102 51.415 +v 52.362 52.623 72.981 +v -91.812 84.277 346.575 +v -91.314 -85.931 51.415 +v 52.594 52.568 72.981 +v 56.323 -91.5 34.5537 +v 54.465 -92 34.116 +v -91.812 -85.277 51.415 +v -91.974 -84.902 51.415 +v 52.332 52.435 72.987 +v 93 -86.95 2.123 +v 52.535 52.386 72.987 +v 55.63 -91.5 34.3181 +v 90.002 71.837 66.03 +v 90.002 71.961 66.946 +v 90.002 72.003 67.884 +v 53.833 -92 33.738 +v 52.727 52.306 72.987 +v 69.826 87.503 66.03 +v 69.958 87.503 66.946 +v 53.242 -92 33.299 +v 70.003 87.503 67.884 +v -92.034 -85.931 52.175 +v -91.59 -85.623 51.415 +v 54.965 -91.5 34.0183 +v 52.697 -92 32.805 +v 92.093 -89.624 2.123 +v 54.333 -91.5 33.6573 +v -92.073 -84.507 51.415 +v 92.482 -89.016 2.123 +v 92.768 -88.356 2.123 +v 92.942 -87.662 2.123 +v 53.198 52.198 72.981 +v 53.742 -91.5 33.2401 +v 52.904 52.198 72.987 +v 53.197 -91.5 32.7689 +v 53.1962 -91.5 32.768 +v 53.063 52.063 72.987 +v -90.879 84.515 347.186 +v 53.198 51.904 72.987 +v 92.046 -89.591 0.696 +v 92.43 -88.99 0.696 +v -90.35 84.882 347.186 +v 52.773 52.017 72.991 +v 92.712 -88.339 0.696 +v 52.904 51.904 72.991 +v -91.092 84.277 347.186 +v 92.884 -87.653 0.696 +v -92.718 -86.407 54.06 +v 53.017 51.773 72.991 +v 92.942 -86.95 0.696 +v -90.408 84.064 347.633 +v 91.5056 -90.0691 0.001 +v -92.496 -87.061 55.14 +v 91.7356 -89.8117 0.001 +v 91.9757 -89.5426 0.001 +v -92.301 -86.875 54.06 +v -90.238 84.202 347.633 +v -90.63 84.719 347.186 +v -92.942 -86.562 55.14 +v 65.677 -92 25.6807 +v 92.3527 -88.9526 0.001 +v 65.677 -91.5 25.6414 +v 65.536 -91.5 25.2662 +v 92.1603 -89.2537 0.001 +v 65.822 -92 26.2314 +v 65.856 -92 26.3607 +v 65.856 -91.5 26.3214 +v 65.8557 -91.5 26.3203 +v 65.8126 -91.5 26.1562 +v 92.4879 -88.6393 0.001 +v 92.6292 -88.3131 0.001 +v 92.712 -87.9838 0.001 +v 92.7982 -87.64 0.001 +v 65.9438 -92 26.9247 +v 65.964 -92 27.0545 +v 65.964 -91.5 27.0152 +v 65.9381 -91.5 26.8488 +v 92.8261 -87.3023 0.001 +v 92.8552 -86.95 0.001 +v 52.297 52.214 72.991 +v 66 -92 27.7553 +v 66 -91.5 27.716 +v 52.466 52.173 72.991 +v -91.263 -87.596 54.06 +v -69.418 52.977 72.955 +v -69.125 53 72.955 +v -69.415 52.954 72.962 +v 52.625 52.107 72.991 +v 65.964 -92 28.4552 +v -90.992 85.195 346.575 +v -91.812 -87.274 54.06 +v 65.964 -91.5 28.4159 +v -69.125 52.977 72.962 +v 52.258 51.966 72.995 +v -69.404 52.886 72.969 +v 52.388 51.935 72.995 +v -90.238 85.562 346.575 +v 65.856 -92 29.1492 +v -69.125 52.908 72.969 +v 65.856 -91.5 29.1099 +v 65.9241 -91.5 28.672 +v -90.754 -88.082 55.14 +v 52.511 51.883 72.995 +v -90.669 -87.831 54.06 +v 65.677 -92 29.83 +v 65.677 -91.5 29.7907 +v -69.386 52.775 72.975 +v 65.536 -91.5 30.1658 +v 52.625 51.814 72.995 +v -69.125 52.796 72.975 +v 65.73 -92 21.158 +v 65.73 -92.497 21.158 +v -90.63 85.407 346.575 +v 52.727 51.727 72.995 +v -69.594 52.568 72.981 +v 90.002 69.155 61.957 +v 90.002 68.627 61.883 +v -69.362 52.623 72.981 +v 90.002 69.67 62.176 +v -91.092 86.274 344.926 +v -91.389 -87.831 55.14 +v 90.002 70.16 62.537 +v 90.002 70.611 63.029 +v -69.125 52.642 72.981 +v 65.132 -93.6845 29.8049 +v 65.177 -93.6887 29.679 +v 90.002 71.014 63.641 +v 53.107 51.625 72.991 +v 65.3025 -93.7057 29.1784 +v 65.356 -93.7134 28.965 +v 90.002 71.358 64.357 +v -91.974 -87.488 55.14 +v 90.002 71.635 65.16 +v -69.535 52.386 72.987 +v 53.173 51.466 72.991 +v -69.332 52.435 72.987 +v -90.408 86.061 345.815 +v -90.552 86.488 344.926 +v 66.402 87.503 61.883 +v 66.966 87.503 61.957 +v 53.214 51.297 72.991 +v -69.125 52.451 72.987 +v 67.515 87.503 62.176 +v 52.814 51.625 72.995 +v -90.113 -88.395 56.277 +v 90.406 -91.004 2.123 +v 64.6289 -93.6426 31.0056 +v 64.9086 -93.6631 30.4152 +v 91.042 -90.631 2.123 +v 64.929 -93.6646 30.372 +v -90.806 -88.236 56.277 +v 91.609 -90.167 2.123 +v 52.883 51.511 72.995 +v 90.38 -90.954 0.696 +v 91.008 -90.586 0.696 +v -91.465 -87.975 56.277 +v 64.2939 -93.6228 31.5713 +v 64.614 -93.6415 31.037 +v 52.935 51.388 72.995 +v 91.567 -90.128 0.696 +v -69.125 52.227 72.991 +v -92.073 -87.618 56.277 +v 52.966 51.258 72.995 +v -92.615 -87.175 56.277 +v 90.3406 -90.8802 0.001 +v 90.6419 -90.703 0.001 +v 64.236 -93.6193 31.668 +v -69.625 52.107 72.991 +v 90.9566 -90.5186 0.001 +v 91.2255 -90.2986 0.001 +v -69.466 52.173 72.991 +v 88.993 -91.444 2.123 +v -69.297 52.214 72.991 +v 89.718 -91.277 2.123 +v 63.4725 -93.586 32.618 +v 63.798 -93.5984 32.259 +v 63.9083 -93.6036 32.1101 +v 88.25 -91.444 0.696 +v 88.984 -91.389 0.696 +v -69.625 51.814 72.995 +v 89.7 -91.224 0.696 +v -69.511 51.883 72.995 +v 52.304 51.676 72.998 +v -69.388 51.935 72.995 +v 88.25 -91.3607 0.001 +v 52.388 51.641 72.998 +v 88.6027 -91.3342 0.001 +v -69.258 51.966 72.995 +v 65.429 -92 30.4898 +v -93.078 -86.657 56.277 +v 52.466 51.594 72.998 +v 88.9705 -91.3067 0.001 +v 65.429 -91.5 30.4505 +v 89.3143 -91.2273 0.001 +v 89.6731 -91.1447 0.001 +v -69.125 51.976 72.995 +v 89.9999 -91.0154 0.001 +v 65.114 -92 31.1235 +v 52.535 51.535 72.998 +v -93.451 -86.075 56.277 +v 65.114 -91.5 31.0842 +v 52.594 51.466 72.998 +v 64.736 -92 31.7252 +v 68.037 87.503 62.537 +v 64.736 -91.5 31.6859 +v 65.0192 -91.5 31.2351 +v 68.518 87.503 63.029 +v 68.948 87.503 63.641 +v 64.298 -92 32.2888 +v 69.315 87.503 64.357 +v 64.298 -91.5 32.2495 +v 64.6443 -91.5 31.8039 +v -93.501 -84.102 54.06 +v 69.61 87.503 65.16 +v -93.054 -85.882 54.06 +v -94.002 83.102 340.547 +v 64.2325 -91.5 32.3181 +v -93.946 83.102 341.713 +v -93.946 83.791 340.547 +v -69.594 51.466 72.998 +v -69.535 51.535 72.998 +v -93.89 83.782 341.713 +v -93.301 -86.002 55.14 +v -69.466 51.594 72.998 +v -93.301 -85.314 54.06 +v -69.388 51.641 72.998 +v -93.723 -84.757 55.14 +v -93.501 85.1 340.547 +v -93.451 -84.716 54.06 +v 52.641 51.388 72.998 +v -69.304 51.676 72.998 +v -93.723 84.445 341.713 +v -93.777 84.462 340.547 +v 52.676 51.304 72.998 +v -69.216 51.697 72.998 +v 93 85.95 2.123 +v -69.125 51.704 72.998 +v 52.697 51.216 72.998 +v -93.124 85.689 340.547 +v -93.563 -85.396 55.14 +v 88.25 90.5 2.123 +v -93.451 85.075 341.713 +v -93.777 -84.102 55.14 +v 52.258 51.386 72.999 +v 65.4366 -93.7972 26.5813 +v -93.078 85.657 341.713 +v 65.1585 -93.8423 25.2724 +v 64.929 -93.8646 24.631 +v 64.9275 -93.8647 24.6278 +v 52.297 51.362 72.999 +v -69.362 51.297 72.999 +v 65.419 -93.7288 28.54 +v -93.723 -85.445 56.277 +v 65.464 -93.7905 26.766 +v 52.332 51.332 72.999 +v -69.332 51.332 72.999 +v -93.777 83.102 342.85 +v 52.362 51.297 72.999 +v -93.723 83.757 342.85 +v -69.297 51.362 72.999 +v 85.402 83.102 49.992 +v 85.402 83.791 50.084 +v 52.386 51.258 72.999 +v 65.356 -93.8157 26.038 +v 65.3276 -93.8195 25.9247 +v 65.177 -93.8405 25.324 +v -69.258 51.386 72.999 +v 85.515 83.782 50.084 +v 52.404 51.216 72.999 +v 85.625 83.757 50.084 +v -69.216 51.404 72.999 +v -93.501 83.102 343.93 +v 65.4809 -93.751 27.893 +v 65.5 -93.7644 27.502 +v 85.825 83.659 50.084 +v -69.171 51.415 72.999 +v 85.911 83.589 50.084 +v 65.464 -93.7392 28.237 +v -93.89 -84.782 56.277 +v -69.125 51.418 72.999 +v -93.451 83.716 343.93 +v -93.946 -84.102 56.277 +v 65.4871 -93.7735 27.2387 +v 52.125 52.977 72.962 +v 65.429 -92 25.0209 +v -89.402 -86.1 50.805 +v 65.429 -91.5 24.9816 +v 65.2277 -91.5 24.576 +v -89.729 -86.075 50.805 +v 52.125 52.908 72.969 +v 52.125 52.796 72.975 +v -89.402 -86.689 51.415 +v -92.942 85.562 342.85 +v 85.729 83.716 50.084 +v -69.404 51.216 72.999 +v 52.125 52.642 72.981 +v -69.386 51.258 72.999 +v -93.301 84.314 343.93 +v -93.563 84.396 342.85 +v 52.125 52.451 72.987 +v 52.125 52.227 72.991 +v -93.054 84.882 343.93 +v -89.402 -87.214 52.175 +v -89.911 -87.175 52.175 +v -93.301 85.002 342.85 +v -89.825 -86.657 51.415 +v 85.985 83.507 50.084 +v 52.125 51.976 72.995 +v 52.125 51.704 72.998 +v -89.402 -87.662 53.064 +v -89.985 -87.618 53.064 +v 52.216 51.697 72.998 +v 85.402 84.462 50.357 +v 52.125 51.418 72.999 +v 52.171 51.415 72.999 +v -92.718 85.407 343.93 +v 52.216 51.404 72.999 +v -89.402 -85.462 50.357 +v -92.301 85.875 343.93 +v -69.386 52.775 70.025 +v -69.125 52.796 70.025 +v -69.404 52.886 70.031 +v -90.048 -85.314 50.357 +v -69.125 52.908 70.031 +v -92.615 86.175 341.713 +v -92.655 86.214 340.547 +v 52.966 52.775 70.038 +v -69.415 52.954 70.038 +v 92.942 86.662 2.123 +v 92.768 87.356 2.123 +v -69.125 52.977 70.038 +v 52.976 52.796 70.045 +v 92.482 88.016 2.123 +v -89.625 -85.445 50.357 +v 92.093 88.624 2.123 +v -69.418 52.977 70.045 +v 91.609 89.167 2.123 +v 91.042 89.631 2.123 +v 90.406 90.004 2.123 +v -69.125 53 70.045 +v -90.048 -86.002 50.805 +v -92.106 86.662 340.547 +v -91.491 87.023 340.547 +v -89.842 -85.396 50.357 +v -90.824 87.287 340.547 +v -90.122 87.448 340.547 +v -69.125 52.227 70.009 +v 52.883 52.614 70.025 +v -92.073 86.618 341.713 +v 52.935 52.714 70.031 +v -69.535 52.386 70.013 +v 53.173 52.568 70.031 +v -69.332 52.435 70.013 +v -90.113 87.395 341.713 +v -69.125 52.451 70.013 +v 92.046 88.592 0.696 +v -89.402 -84.791 50.084 +v 53.214 52.623 70.038 +v -89.515 -84.782 50.084 +v -69.594 52.568 70.019 +v -91.465 86.975 341.713 +v -89.625 -84.757 50.084 +v 53.227 52.642 70.045 +v -89.825 -84.659 50.084 +v -69.362 52.623 70.019 +v -89.911 -84.589 50.084 +v -90.806 87.236 341.713 +v -69.125 52.642 70.019 +v 85.402 85.1 50.805 +v 52.814 52.477 70.019 +v 85.729 85.075 50.805 +v 85.625 84.445 50.357 +v 53.017 52.352 70.019 +v -89.729 -84.716 50.084 +v -92.496 86.061 342.85 +v -69.625 51.814 70.005 +v 85.842 84.396 50.357 +v -69.511 51.883 70.005 +v 53.107 52.477 70.025 +v -91.389 86.831 342.85 +v -69.388 51.935 70.005 +v -90.754 87.082 342.85 +v -69.258 51.966 70.005 +v -89.985 -84.507 50.084 +v 85.402 85.689 51.415 +v -69.125 51.976 70.005 +v -91.812 86.274 343.93 +v 52.386 52.775 70.025 +v -91.974 86.488 342.85 +v -69.625 52.107 70.009 +v -89.402 -84.102 49.992 +v 52.404 52.886 70.031 +v 85.825 85.657 51.415 +v 52.676 52.821 70.031 +v -69.466 52.173 70.009 +v -90.669 86.831 343.93 +v 52.415 52.954 70.038 +v -69.297 52.214 70.009 +v 52.697 52.886 70.038 +v -90.044 -84.415 50.084 +v 85.402 86.214 52.175 +v -90.087 -84.315 50.084 +v 85.911 86.175 52.175 +v -91.263 86.596 343.93 +v 52.418 52.977 70.045 +v 85.402 86.662 53.064 +v 85.985 86.618 53.064 +v 52.704 52.908 70.045 +v -69.594 51.466 70.002 +v -89.402 87.503 340.547 +v -69.535 51.535 70.002 +v 52.332 52.435 70.013 +v 85.402 87.023 54.06 +v 91.567 89.128 0.696 +v 52.535 52.386 70.013 +v -69.466 51.594 70.002 +v 91.008 89.586 0.696 +v 52.727 52.306 70.013 +v -89.402 87.287 342.85 +v -89.402 87.448 341.713 +v -69.388 51.641 70.002 +v 90.38 89.954 0.696 +v 52.362 52.623 70.019 +v 85.402 87.287 55.14 +v -90.044 86.975 343.93 +v 52.594 52.568 70.019 +v -69.304 51.676 70.002 +v -90.087 87.236 342.85 +v -89.402 -88.023 54.06 +v -90.044 -87.975 54.06 +v 85.402 87.448 56.277 +v -89.402 87.023 343.93 +v -69.216 51.697 70.002 +v -89.402 -88.287 55.14 +v 52.641 52.714 70.025 +v -69.125 51.704 70.002 +v -89.402 -88.448 56.277 +v -90.087 -88.236 55.14 +v 86.044 83.415 50.084 +v 86.087 83.315 50.084 +v 52.773 52.017 70.009 +v 86.113 83.21 50.084 +v 86.122 83.102 50.084 +v 52.904 51.904 70.009 +v 53.017 51.773 70.009 +v 52.904 52.198 70.013 +v 53.063 52.063 70.013 +v -69.362 51.297 70.001 +v 53.198 51.904 70.013 +v -69.332 51.332 70.001 +v -69.297 51.362 70.001 +v -69.258 51.386 70.001 +v -69.216 51.404 70.001 +v -69.171 51.415 70.001 +v -90.122 -88.448 57.443 +v -69.125 51.418 70.001 +v -90.824 -88.287 57.443 +v 53.198 52.198 70.019 +v -91.491 -88.023 57.443 +v 86.552 83.902 50.357 +v 58.5 -93.751 27.893 +v -92.106 -87.662 57.443 +v -92.655 -87.214 57.443 +v 52.258 51.966 70.005 +v -93.124 -86.689 57.443 +v 86.754 83.523 50.357 +v 92.1681 88.2415 0.001 +v 52.388 51.935 70.005 +v -93.501 -86.1 57.443 +v 91.9757 88.5431 0.001 +v 91.5056 89.0691 0.001 +v 91.7457 88.8006 0.001 +v 52.511 51.883 70.005 +v 91.2367 89.2892 0.001 +v 90.9566 89.5186 0.001 +v -69.404 51.216 70.001 +v 90.3406 89.8802 0.001 +v -69.386 51.258 70.001 +v 90.6553 89.6958 0.001 +v 52.625 51.814 70.005 +v 86.669 83.72 50.357 +v 52.727 51.727 70.005 +v 92.884 86.653 0.696 +v 52.297 52.214 70.009 +v 92.712 87.339 0.696 +v -94.002 -84.102 57.443 +v -93.777 -85.462 57.443 +v 52.466 52.173 70.009 +v -94.002 72.003 295.948 +v -93.946 -84.791 57.443 +v -94.002 71.961 296.886 +v -94.002 71.837 297.802 +v 86.806 83.315 50.357 +v -69.418 51.125 72.999 +v -94.002 71.635 298.672 +v -69.415 51.171 72.999 +v 52.625 52.107 70.009 +v -94.002 71.358 299.475 +v -69.125 51.125 73 +v -94.002 71.014 300.191 +v 92.43 87.99 0.696 +v 86.824 83.102 50.357 +v -94.002 70.611 300.803 +v -94.002 70.16 301.295 +v -94.002 69.67 301.656 +v -69.418 51.125 70.001 +v -69.125 51.125 70 +v -94.002 69.155 301.875 +v -69.415 51.171 70.001 +v -94.002 68.627 301.949 +v 87.465 83.415 50.805 +v -89.402 -88.502 57.443 +v -73.958 87.503 296.886 +v -74.003 87.503 295.948 +v -73.826 87.503 297.802 +v -73.61 87.503 298.672 +v -73.315 87.503 299.475 +v 52.814 51.625 70.005 +v 87.389 83.72 50.805 +v -72.948 87.503 300.191 +v -69.976 52.796 72.955 +v -72.518 87.503 300.803 +v -69.704 52.908 72.955 +v 52.883 51.511 70.005 +v -72.037 87.503 301.295 +v -69.966 52.775 72.962 +v -71.515 87.503 301.656 +v 52.935 51.388 70.005 +v -69.697 52.886 72.962 +v 87.491 83.102 50.805 +v 52.966 51.258 70.005 +v -69.676 52.821 72.969 +v 87.974 83.902 51.415 +v 92.8272 86.2878 0.001 +v 92.7982 86.64 0.001 +v 92.7154 86.9693 0.001 +v 53.107 51.625 70.009 +v 92.6292 87.3131 0.001 +v 92.3527 87.9526 0.001 +v 92.494 87.6264 0.001 +v 53.173 51.466 70.009 +v 53.214 51.297 70.009 +v -69.935 52.714 72.969 +v 92.942 85.95 0.696 +v -70.107 52.477 72.975 +v 88.106 83.102 51.415 +v -69.883 52.614 72.975 +v -69.641 52.714 72.975 +v 52.304 51.676 70.002 +v -70.017 52.352 72.981 +v 52.388 51.641 70.002 +v -69.814 52.477 72.981 +v 52.466 51.594 70.002 +v 52.535 51.535 70.002 +v 52.594 51.466 70.002 +v -69.727 52.306 72.987 +v -70.642 52.227 72.955 +v -96.942 -87.662 2.123 +v -70.451 52.451 72.955 +v -96.768 -88.356 2.123 +v -70.227 52.642 72.955 +v -96.482 -89.016 2.123 +v -96.093 -89.624 2.123 +v -70.435 52.435 72.962 +v -70.214 52.623 72.962 +v 52.258 51.386 70.001 +v 52.297 51.362 70.001 +v 52.332 51.332 70.001 +v 52.362 51.297 70.001 +v -70.386 52.386 72.969 +v 52.386 51.258 70.001 +v -96.942 -86.95 0.696 +v -96.884 -87.653 0.696 +v 52.404 51.216 70.001 +v -70.173 52.568 72.969 +v -70.306 52.306 72.975 +v -96.712 -88.339 0.696 +v -96.43 -88.99 0.696 +v -89.402 -87.662 344.926 +v -96.046 -89.591 0.696 +v 52.641 51.388 70.002 +v -89.402 -87.214 345.815 +v 52.676 51.304 70.002 +v -89.911 -87.175 345.815 +v -89.985 -87.618 344.926 +v -96.8552 -86.95 0.001 +v -96.8272 -87.2878 0.001 +v 52.697 51.216 70.002 +v -70.063 52.063 72.987 +v -96.7982 -87.64 0.001 +v -96.7154 -87.9693 0.001 +v -89.402 -86.689 346.575 +v -69.904 52.198 72.987 +v -96.6292 -88.3131 0.001 +v -96.3527 -88.9526 0.001 +v -96.494 -88.6264 0.001 +v -96.1681 -89.2415 0.001 +v -70.017 51.773 72.991 +v -95.9757 -89.5426 0.001 +v -89.402 -86.1 347.186 +v -89.729 -86.075 347.186 +v -89.825 -86.657 346.575 +v -95.7457 -89.8 0.001 +v -69.904 51.904 72.991 +v -95.5056 -90.0691 0.001 +v -69.773 52.017 72.991 +v 52.125 52.227 70.009 +v 52.125 52.451 70.013 +v -69.418 -70.125 72.999 +v -69.125 -70.125 73 +v -90.048 -86.002 347.186 +v 52.125 52.642 70.019 +v -93.718 -91.277 2.123 +v -92.993 -91.444 2.123 +v -69.727 51.727 72.995 +v -89.402 -85.462 347.633 +v -93.7 -91.224 0.696 +v -92.984 -91.389 0.696 +v 88.073 83.507 51.415 +v 52.125 52.796 70.025 +v 92.8552 85.95 0.001 +v -92.25 -91.444 0.696 +v 88.655 83.102 52.175 +v -69.258 -70.386 72.999 +v 89.718 90.277 2.123 +v -94.0138 -91.0094 0.001 +v 88.993 90.444 2.123 +v 52.125 52.908 70.031 +v -93.6731 -91.1447 0.001 +v 89.7 90.224 0.696 +v -93.3293 -91.224 0.001 +v -92.9705 -91.3067 0.001 +v -69.297 -70.362 72.999 +v 52.125 52.977 70.038 +v 88.984 90.389 0.696 +v 89.078 83.659 53.064 +v -92.6178 -91.3331 0.001 +v -92.25 -91.3607 0.001 +v 88.615 83.589 52.175 +v -70.623 52.214 72.962 +v 88.25 90.444 0.696 +v -69.332 -70.332 72.999 +v -70.568 52.173 72.969 +v -95.609 -90.167 2.123 +v 90.0138 90.0099 0.001 +v -95.042 -90.631 2.123 +v -69.362 -70.297 72.999 +v 89.6731 90.1452 0.001 +v -94.406 -91.004 2.123 +v 89.3293 90.224 0.001 +v -69.386 -70.258 72.999 +v -95.567 -90.128 0.696 +v 88.9705 90.3067 0.001 +v 52.125 51.418 70.001 +v 88.6178 90.3331 0.001 +v 88.25 90.3607 0.001 +v 52.171 51.415 70.001 +v -69.404 -70.216 72.999 +v -95.008 -90.586 0.696 +v -70.477 52.107 72.975 +v 52.216 51.404 70.001 +v 89.124 83.102 53.064 +v -69.415 -70.171 72.999 +v -94.38 -90.954 0.696 +v 52.125 51.704 70.002 +v 86.048 84.314 50.357 +v 52.216 51.697 70.002 +v -70.352 52.017 72.981 +v -95.2367 -90.2892 0.001 +v 86.238 84.202 50.357 +v -70.198 52.198 72.981 +v -94.9566 -90.5186 0.001 +v -94.3406 -90.8802 0.001 +v -94.6553 -90.6958 0.001 +v 66.315 -92 33.057 +v 66.315 -92.497 33.057 +v 52.125 51.976 70.005 +v 67.808 -92 29.452 +v 67.808 -92.497 29.452 +v -70.614 51.883 72.975 +v -71 51.125 72.955 +v 67.569 -92 30.404 +v 67.569 -92.497 30.404 +v 67.239 -92 31.328 +v 67.239 -92.497 31.328 +v 86.35 84.882 50.805 +v 66.819 -92 32.215 +v 66.819 -92.497 32.215 +v -70.477 51.814 72.981 +v -69.388 -70.641 72.998 +v -71 51.125 70.045 +v -69.466 -70.594 72.998 +v 53.227 51.125 72.991 +v 86.408 84.064 50.357 +v -69.535 -70.535 72.998 +v -70.306 51.727 72.987 +v 52.976 51.125 72.995 +v -70.198 51.904 72.987 +v -69.594 -70.466 72.998 +v 67.569 -92 24.599 +v 67.569 -92.497 24.599 +v 67.808 -92 25.551 +v 67.808 -92.497 25.551 +v 67.952 -92 26.521 +v 67.952 -92.497 26.521 +v 52.704 51.125 72.998 +v 68 -92 27.502 +v 68 -92.497 27.502 +v 67.952 -92 28.482 +v 67.952 -92.497 28.482 +v -69.125 -70.418 72.999 +v -69.171 -70.415 72.999 +v 52.415 51.171 72.999 +v -70.107 51.625 72.991 +v -69.216 -70.404 72.999 +v 52.418 51.125 72.999 +v 86.63 84.719 50.805 +v -69.883 51.511 72.995 +v -69.814 51.625 72.995 +v 86.879 84.515 50.805 +v 66.315 -92 21.946 +v 66.315 -92.497 21.946 +v 66.819 -92 22.788 +v 66.819 -92.497 22.788 +v 67.239 -92 23.675 +v 67.239 -92.497 23.675 +v -69.966 51.258 72.995 +v -69.935 51.388 72.995 +v 52.125 51.125 70 +v 88.25 -91.5 348.873 +v 87.092 84.277 50.805 +v -69.697 51.216 72.998 +v 87.263 84.009 50.805 +v -69.676 51.304 72.998 +v 69.958 -91.5 296.886 +v 70.003 -91.5 295.948 +v 69.826 -91.5 297.802 +v 69.61 -91.5 298.672 +v 69.315 -91.5 299.475 +v 52.415 51.171 70.001 +v 68.948 -91.5 300.191 +v 52.418 51.125 70.001 +v 68.518 -91.5 300.803 +v -69.641 51.388 72.998 +v 68.037 -91.5 301.295 +v 67.515 -91.5 301.656 +v 52.704 51.125 70.002 +v 66.966 -91.5 301.875 +v 66.402 -91.5 301.949 +v -69.511 -70.883 72.995 +v 52.976 51.125 70.005 +v -69.625 -70.814 72.995 +v 69.826 -91.5 66.03 +v 69.958 -91.5 66.946 +v 53.227 51.125 70.009 +v 70.003 -91.5 67.884 +v -69.125 -70.704 72.998 +v -70.623 51.362 72.981 +v 66.966 -91.5 61.957 +v 66.402 -91.5 61.883 +v -69.216 -70.697 72.998 +v 67.515 -91.5 62.176 +v 68.037 -91.5 62.537 +v -70.568 51.594 72.981 +v 68.518 -91.5 63.029 +v 86.048 85.002 50.805 +v -69.304 -70.676 72.998 +v -90.048 -85.314 347.633 +v -70.435 51.332 72.987 +v -70.386 51.535 72.987 +v -89.625 -85.445 347.633 +v 87.314 84.931 51.415 +v -70.214 51.297 72.991 +v -89.842 -85.396 347.633 +v 87.812 84.277 51.415 +v -70.173 51.466 72.991 +v -69.625 -71.107 72.991 +v -69.125 -70.976 72.995 +v 88.034 84.931 52.175 +v -69.258 -70.966 72.995 +v 87.59 84.623 51.415 +v -69.388 -70.935 72.995 +v -89.402 -84.791 347.906 +v -70.796 51.976 72.955 +v -89.515 -84.782 347.906 +v -70.775 51.966 72.962 +v -89.625 -84.757 347.906 +v -70.714 51.935 72.969 +v -89.729 -84.716 347.906 +v 86.238 85.562 51.415 +v 86.63 85.407 51.415 +v -89.825 -84.659 347.906 +v -69.125 -71.642 72.981 +v -69.362 -71.623 72.981 +v -89.911 -84.589 347.906 +v -69.594 -71.568 72.981 +v -89.985 -84.507 347.906 +v -69.125 -71.451 72.987 +v -69.332 -71.435 72.987 +v -70.977 51.418 72.955 +v -70.908 51.704 72.955 +v -69.535 -71.386 72.987 +v 86.879 85.875 52.175 +v -70.954 51.415 72.962 +v -70.886 51.697 72.962 +v 86.992 85.195 51.415 +v 87.702 85.302 52.175 +v -69.125 -71.227 72.991 +v -70.886 51.404 72.969 +v -69.297 -71.214 72.991 +v -70.821 51.676 72.969 +v -90.044 -84.415 347.906 +v -69.466 -71.173 72.991 +v -90.087 -84.315 347.906 +v 87.314 85.62 52.175 +v 62.035 -93.971 21.608 +v -70.775 51.386 72.975 +v -89.402 -84.102 347.998 +v -70.714 51.641 72.975 +v -69.125 -72 72.955 +v -69.418 -71.977 72.955 +v 87.59 85.982 53.064 +v -69.125 -71.977 72.962 +v 68.948 -91.5 63.641 +v -69.415 -71.954 72.962 +v 63.803 -93.927 22.854 +v 63.258 -93.943 22.384 +v 64.2709 -93.9083 23.3932 +v 64.236 -93.9103 23.335 +v -69.125 -71.908 72.969 +v 64.1439 -93.9146 23.2108 +v -69.404 -71.886 72.969 +v -69.125 -71.796 72.975 +v -69.386 -71.775 72.975 +v 69.315 -91.5 64.357 +v 69.61 -91.5 65.16 +v 88.034 85.62 53.064 +v 64.6306 -93.8867 24.0009 +v 64.614 -93.8879 23.966 +v 68 -91.5 37.4957 +v -69.676 52.821 70.031 +v -71 -70.125 72.955 +v -69.966 52.775 70.038 +v 50.929 -92.497 34.573 +v 50.929 -92 34.573 +v 88.496 84.064 52.175 +v 50.27 -92 33.845 +v 50.27 -92.497 33.845 +v 49.685 -92 33.057 +v 49.685 -92.497 33.057 +v -69.697 52.886 70.038 +v -69.125 -70.125 70 +v -69.976 52.796 70.045 +v 62.667 -93.958 21.967 +v 88.301 84.515 52.175 +v -69.704 52.908 70.045 +v 49.181 -92.497 32.215 +v 49.181 -92 32.215 +v 88.942 84.202 53.064 +v -96.7981 -87.64 350.995 +v -96.8261 -87.302 350.995 +v -96.884 -87.653 350.3 +v -69.418 -70.125 70.001 +v -96.8551 -86.95 350.995 +v -96.942 -86.95 350.3 +v -96.712 -87.9835 350.995 +v -69.727 52.306 70.013 +v 50.644 -92 28.965 +v -70.017 52.352 70.019 +v 50.823 -92 29.679 +v -69.814 52.477 70.019 +v 88.413 85.195 53.064 +v -69.258 -70.386 70.001 +v -69.297 -70.362 70.001 +v -69.332 -70.332 70.001 +v -69.362 -70.297 70.001 +v -69.386 -70.258 70.001 +v 62.758 -92 21.704 +v -69.404 -70.216 70.001 +v 88.718 84.719 53.064 +v -69.415 -70.171 70.001 +v 62.167 -92 21.266 +v -70.107 52.477 70.025 +v 61.535 -92 20.887 +v 50.644 -94.997 28.965 +v 50.823 -94.997 29.679 +v -69.883 52.614 70.025 +v -96.712 -88.339 350.3 +v -96.6291 -88.313 350.995 +v -69.641 52.714 70.025 +v -96.43 -88.99 350.3 +v -96.4879 -88.639 350.995 +v 86.408 86.061 52.175 +v -96.3526 -88.9526 350.995 +v -95.9756 -89.5426 350.995 +v -96.1603 -89.2535 350.995 +v -96.046 -89.591 350.3 +v 65.071 -92 20.43 +v 87.092 86.274 53.064 +v 65.071 -92.497 20.43 +v -69.935 52.714 70.031 +v -95.5056 -90.0691 350.995 +v -95.7358 -89.8115 350.995 +v -69.125 -70.418 70.001 +v -69.171 -70.415 70.001 +v 86.552 86.488 53.064 +v -69.216 -70.404 70.001 +v 62.035 -91.5 21.4137 +v 61.44 -91.5 21.1455 +v -69.388 -70.641 70.002 +v -69.466 -70.594 70.002 +v 50.823 -94.997 25.324 +v -69.535 -70.535 70.002 +v 50.823 -92 25.324 +v 50.644 -94.997 26.038 +v 50.644 -92 26.038 +v 89.501 83.102 54.06 +v -69.594 -70.466 70.002 +v 89.723 83.757 55.14 +v -70.173 52.568 70.031 +v 89.451 83.716 54.06 +v 93 -86.95 348.873 +v 62.667 -91.5 21.7738 +v 50.536 -94.997 26.766 +v 50.536 -92 26.766 +v -70.214 52.623 70.038 +v 50.5 -94.997 27.502 +v 63.303 -92 22.198 +v 50.5 -92 27.502 +v -70.227 52.642 70.045 +v 63.258 -91.5 22.1919 +v 89.777 83.102 55.14 +v 50.536 -94.997 28.237 +v 63.803 -92.6136 22.7507 +v 63.798 -92.5828 22.744 +v 50.536 -92 28.237 +v 63.6058 -92 22.532 +v 63.803 -92 22.7025 +v -70.306 52.306 70.025 +v 88.301 85.875 54.06 +v 63.803 -91.5 22.6632 +v 63.6466 -91.5 22.528 +v 63.4408 -91.5 22.3501 +v 88.718 85.407 54.06 +v -70.386 52.386 70.031 +v -69.125 -70.704 70.002 +v -69.216 -70.697 70.002 +v 89.054 84.882 54.06 +v 64.298 -92 23.2219 +v 64.298 -91.5 23.1826 +v 92.7152 -87.9695 350.995 +v 92.884 -87.653 350.3 +v -69.304 -70.676 70.002 +v 64.736 -92 23.7855 +v 92.7981 -87.64 350.995 +v -70.435 52.435 70.038 +v 64.736 -91.5 23.7462 +v 92.8272 -87.288 350.995 +v 92.942 -86.95 350.3 +v 65.114 -92 24.3861 +v 65.114 -91.5 24.3468 +v 64.8028 -91.5 23.8523 +v 92.8551 -86.95 350.995 +v 88.942 85.562 55.14 +v -69.511 -70.883 70.005 +v 49.181 -92.497 22.788 +v 49.181 -92 22.788 +v -70.642 52.227 70.045 +v 57.765 -93.999 20.801 +v -69.625 -70.814 70.005 +v 58.5 -94 20.766 +v 89.301 84.314 54.06 +v -70.451 52.451 70.045 +v 91.5056 -90.0691 350.995 +v 91.7454 -89.8002 350.995 +v 59.235 -93.999 20.801 +v 92.046 -89.591 350.3 +v 49.685 -92 21.946 +v 49.685 -92.497 21.946 +v 50.27 -92 21.158 +v 50.27 -92.497 21.158 +v 61.37 -93.981 21.309 +v 91.9756 -89.5426 350.995 +v 60.677 -93.989 21.073 +v 92.1679 -89.2416 350.995 +v 92.43 -88.99 350.3 +v 92.3526 -88.9526 350.995 +v 92.712 -88.339 350.3 +v 92.6291 -88.313 350.995 +v 59.963 -93.995 20.903 +v 92.4938 -88.6266 350.995 +v -69.727 51.727 70.005 +v -69.125 -70.976 70.005 +v 89.301 85.002 55.14 +v -69.258 -70.966 70.005 +v -70.017 51.773 70.009 +v -69.388 -70.935 70.005 +v 60.87 -92 20.572 +v 50.929 -92 20.43 +v 50.929 -92.497 20.43 +v -69.904 51.904 70.009 +v 89.563 84.396 55.14 +v 48.761 -92 31.328 +v 48.761 -92.497 31.328 +v 60.177 -92 20.324 +v 48.431 -92 30.404 +v 48.431 -92.497 30.404 +v 48.192 -92 29.452 +v 48.192 -92.497 29.452 +v 59.463 -92 20.146 +v -69.773 52.017 70.009 +v -96.942 -87.662 348.873 +v 58.735 -92 20.038 +v -69.625 -71.107 70.009 +v -96.768 -88.356 348.873 +v -70.063 52.063 70.013 +v 48.048 -92 28.482 +v 48.048 -92.497 28.482 +v 48 -92 27.502 +v 48 -92.497 27.502 +v 48.048 -92 26.521 +v 48.048 -92.497 26.521 +v -69.904 52.198 70.013 +v 48.192 -92 25.551 +v 48.192 -92.497 25.551 +v 59.235 -91.5 20.6045 +v 58.5 -91.5 20.5695 +v 48.431 -92 24.599 +v 48.431 -92.497 24.599 +v 89.723 84.445 56.277 +v -96.482 -89.016 348.873 +v -69.125 -71.227 70.009 +v -96.093 -89.624 348.873 +v -69.297 -71.214 70.009 +v 59.963 -91.5 20.7069 +v 48.761 -92 23.675 +v 48.761 -92.497 23.675 +v 60.677 -91.5 20.8773 +v -69.466 -71.173 70.009 +v -70.352 52.017 70.019 +v 61.37 -91.5 21.114 +v 23.9091 -91.5 341.0011 +v 57.765 -91.5 20.6045 +v 57.344 -91.5 20.6637 +v 89.078 85.657 56.277 +v -70.198 52.198 70.019 +v 92.093 -89.624 348.873 +v -69.125 -71.451 70.013 +v 22.8537 -91.5 306.0905 +v 92.482 -89.016 348.873 +v -69.332 -71.435 70.013 +v -26.8537 -91.5 306.0905 +v 92.768 -88.356 348.873 +v -27.9091 -91.5 341.0011 +v -70.477 52.107 70.025 +v 89.451 85.075 56.277 +v -69.535 -71.386 70.013 +v 92.942 -87.662 348.873 +v -70.966 -91.5 301.875 +v -70.402 -91.5 301.949 +v -69.125 -71.642 70.019 +v 89.89 83.782 56.277 +v -70.568 52.173 70.031 +v -69.362 -71.623 70.019 +v -70.623 52.214 70.038 +v 89.946 83.102 56.277 +v -69.594 -71.568 70.019 +v 52.702 -93.593 32.414 +v 52.264 -93.613 31.852 +v 90.38 -90.954 350.3 +v 90.3406 -90.8801 350.995 +v 91.008 -90.586 350.3 +v 90.9566 -90.5186 350.995 +v 90.655 -90.6958 350.995 +v 51.886 -93.634 31.252 +v 86.044 86.975 54.06 +v -70.306 51.727 70.013 +v 51.571 -93.656 30.62 +v 51 -91.5 27.716 +v 51.323 -93.679 29.962 +v 91.2365 -90.2893 350.995 +v 87.263 86.596 54.06 +v 91.567 -90.128 350.3 +v -69.125 -71.796 70.025 +v -70.198 51.904 70.013 +v -69.386 -71.775 70.025 +v -70.477 51.814 70.019 +v 90.406 -91.004 348.873 +v 51.144 -93.702 29.283 +v 91.042 -90.631 348.873 +v -69.125 -71.908 70.031 +v 91.609 -90.167 348.873 +v -92.413 -86.195 344.926 +v -69.404 -71.886 70.031 +v 87.812 86.274 54.06 +v -69.125 -71.977 70.038 +v -70.614 51.883 70.025 +v 86.087 87.236 55.14 +v -69.415 -71.954 70.038 +v 86.754 87.082 55.14 +v 86.669 86.831 54.06 +v -69.883 51.511 70.005 +v -91.092 -87.274 344.926 +v -69.125 -72 70.045 +v -69.814 51.625 70.005 +v -69.418 -71.977 70.045 +v -70.402 -91.5 61.883 +v -70.966 -91.5 61.957 +v -92.034 -86.619 344.926 +v 88.496 86.061 55.14 +v -70.107 51.625 70.009 +v -70.642 -70.125 72.981 +v -90.408 -87.061 345.815 +v -90.552 -87.488 344.926 +v 17.0337 -88.502 340.547 +v 17.0045 -88.4842 340.932 +v -70.451 -70.125 72.987 +v -70.227 -70.125 72.991 +v 87.389 86.831 55.14 +v -69.976 -70.125 72.995 +v -91.314 -86.619 345.815 +v -91.59 -86.982 344.926 +v -69.704 -70.125 72.998 +v -69.697 51.216 70.002 +v 87.974 86.488 55.14 +v -69.676 51.304 70.002 +v -69.935 -70.388 72.995 +v -69.641 51.388 70.002 +v -69.966 -70.258 72.995 +v -69.966 51.258 70.005 +v 15.9899 -88.502 306.0219 +v 51.764 -92 31.668 +v -69.935 51.388 70.005 +v 52.202 -92 32.259 +v -90.879 -86.875 345.815 +v 88.25 -91.3606 350.995 +v 88.25 -91.444 350.3 +v 88.9705 -91.3066 350.995 +v 88.6175 -91.3331 350.995 +v -69.641 -70.388 72.998 +v -21.0045 -88.4842 340.932 +v -21.0337 -88.502 340.547 +v 51.386 -92 31.037 +v -69.676 -70.304 72.998 +v -91.702 -86.302 345.815 +v -69.697 -70.216 72.998 +v 52.702 -91.5 32.2495 +v -70.214 51.297 70.009 +v 86.806 87.236 56.277 +v -70.173 51.466 70.009 +v -90.238 -86.562 346.575 +v 51.071 -92 30.372 +v 88.984 -91.389 350.3 +v -19.9899 -88.502 306.0219 +v 52.264 -91.5 31.6859 +v 89.7 -91.224 350.3 +v 89.673 -91.1446 350.995 +v -70.435 51.332 70.013 +v 89.329 -91.224 350.995 +v -70.107 -70.625 72.991 +v -70.386 51.535 70.013 +v 90.0135 -91.0094 350.995 +v -69.814 -70.625 72.995 +v 51.886 -91.5 31.0842 +v 51.571 -91.5 30.4505 +v -69.883 -70.511 72.995 +v 51.323 -91.5 29.7907 +v 88.993 -91.444 348.873 +v 51.144 -91.5 29.1099 +v 51.0759 -91.5 28.672 +v 89.718 -91.277 348.873 +v -70.623 51.362 70.019 +v -70.568 51.594 70.019 +v -90.63 -86.407 346.575 +v -90.992 -86.195 346.575 +v -70.966 -88.502 301.875 +v -70.402 -88.502 301.949 +v 85.985 -87.618 344.926 +v -70.714 51.935 70.031 +v 85.402 -87.662 344.926 +v -70.775 51.966 70.038 +v -70.568 -70.594 72.981 +v -70.796 51.976 70.045 +v -92.718 -85.718 344.926 +v -70.623 -70.362 72.981 +v 85.911 -87.175 345.815 +v 87.465 86.975 56.277 +v 88.073 86.618 56.277 +v -93.078 -84.659 344.926 +v -70.775 51.386 70.025 +v 88.615 86.175 56.277 +v -70.386 -70.535 72.987 +v -70.714 51.641 70.025 +v -70.435 -70.332 72.987 +v 86.113 87.395 56.277 +v 51.036 -93.727 28.591 +v -92.301 -85.515 345.815 +v -70.886 51.404 70.031 +v -70.821 51.676 70.031 +v 51.144 -93.8 26.502 +v -92.942 -85.202 344.926 +v 51.323 -93.823 25.824 +v 51.571 -93.846 25.166 +v -70.954 51.415 70.038 +v -92.615 -84.589 345.815 +v -70.173 -70.466 72.991 +v -70.402 -88.502 61.883 +v -70.966 -88.502 61.957 +v -70.886 51.697 70.038 +v -70.214 -70.297 72.991 +v -93.124 -84.102 344.926 +v -70.977 51.418 70.045 +v -70.908 51.704 70.045 +v 51 -93.751 27.893 +v 51.036 -93.775 27.194 +v -92.496 -85.064 345.815 +v -70.977 -70.418 72.955 +v -70.642 51.125 72.981 +v -70.954 -70.415 72.962 +v 51.036 -91.5 28.4159 +v -70.451 51.125 72.987 +v 85.402 -86.689 346.575 +v -70.977 -70.125 72.962 +v 85.402 -87.214 345.815 +v -92.655 -84.102 345.815 +v -70.227 51.125 72.991 +v 51.071 -92 24.631 +v -70.886 -70.404 72.969 +v -69.976 51.125 72.995 +v -70.908 -70.125 72.969 +v -92.034 -85.931 345.815 +v -69.704 51.125 72.998 +v 85.729 -86.075 347.186 +v 51.036 -91.5 27.0152 +v 85.825 -86.657 346.575 +v -70.714 -70.641 72.975 +v 51.144 -91.5 26.3214 +v 51.1437 -91.5 26.3233 +v 51.1179 -91.5 26.4887 +v -70.775 -70.386 72.975 +v -70.977 51.125 72.962 +v -70.796 -70.125 72.975 +v 51.323 -91.5 25.6414 +v 51.2793 -91.5 25.8074 +v 51.571 -91.5 24.9816 +v 51.7723 -91.5 24.576 +v -70.908 51.125 72.969 +v -70.796 51.125 72.975 +v 85.402 -86.1 347.186 +v -91.314 -85.931 346.575 +v -70.063 -71.063 72.987 +v 54.965 -93.971 21.608 +v -69.773 -71.017 72.991 +v 85.402 87.503 57.443 +v -69.904 -70.904 72.991 +v 54.333 -93.958 21.967 +v -69.704 51.125 70.002 +v 53.742 -93.943 22.384 +v 57.037 -93.995 20.903 +v -70.017 -70.773 72.991 +v 56.323 -93.989 21.073 +v -91.59 -85.623 346.575 +v 55.63 -93.981 21.309 +v 85.625 -85.445 347.633 +v -69.976 51.125 70.005 +v -91.263 -85.009 347.186 +v -70.227 51.125 70.009 +v -91.812 -85.277 346.575 +v 85.402 -85.462 347.633 +v -69.727 -70.727 72.995 +v -70.451 51.125 70.013 +v 55.13 -92 20.572 +v 54.465 -92 20.887 +v 53.833 -92 21.266 +v 85.842 -85.396 347.633 +v -70.642 51.125 70.019 +v -90.35 -85.882 347.186 +v 55.823 -92 20.324 +v -90.879 -85.515 347.186 +v -70.477 -71.107 72.975 +v 53.742 -91.5 22.1919 +v 53.248 -91.5 22.6191 +v -70.796 51.125 70.025 +v 89.124 85.689 57.443 +v -70.614 -70.883 72.975 +v 89.501 85.1 57.443 +v -70.908 51.125 70.031 +v 89.777 84.462 57.443 +v 56.537 -92 20.146 +v -70.352 -71.017 72.981 +v -90.238 -85.202 347.633 +v -70.977 51.125 70.038 +v 89.946 83.791 57.443 +v -90.63 -85.718 347.186 +v -70.477 -70.814 72.981 +v 90.002 83.102 57.443 +v 54.333 -91.5 21.7738 +v 57.265 -92 20.038 +v -70.198 -70.904 72.987 +v -91.092 -85.277 347.186 +v -70.306 -70.727 72.987 +v 54.965 -91.5 21.4137 +v 86.122 87.448 57.443 +v 86.824 87.287 57.443 +v 55.63 -91.5 21.114 +v 56.323 -91.5 20.8773 +v 87.491 87.023 57.443 +v 57.037 -91.5 20.7069 +v 88.106 86.662 57.443 +v -90.408 -85.064 347.633 +v 88.655 86.214 57.443 +v 85.985 -84.507 347.906 +v -69.883 -71.614 72.975 +v -92.106 -84.102 346.575 +v -91.974 -84.902 346.575 +v -70.107 -71.477 72.975 +v 53.977 -70.125 72.962 +v -69.814 -71.477 72.981 +v 52.264 -93.889 23.934 +v 52.702 -93.909 23.372 +v -70.017 -71.352 72.981 +v -91.491 -84.102 347.186 +v 53.908 -70.125 72.969 +v 53.796 -70.125 72.975 +v -91.465 -84.415 347.186 +v -69.727 -71.306 72.987 +v -92.073 -84.507 346.575 +v 53.197 -93.927 22.854 +v -69.904 -71.198 72.987 +v 53.642 -70.125 72.981 +v -70.966 90.5 61.957 +v 53.451 -70.125 72.987 +v -70.402 90.5 61.883 +v 85.911 -84.589 347.906 +v 85.825 -84.659 347.906 +v -91.389 -84.72 347.186 +v -70.642 -71.227 72.955 +v 53.977 -70.418 72.955 +v -70.623 -71.214 72.962 +v 53.954 -70.415 72.962 +v 85.729 -84.716 347.906 +v 53.886 -70.404 72.969 +v 51.886 -93.868 24.533 +v 85.625 -84.757 347.906 +v -70.386 -71.386 72.969 +v 85.515 -84.782 347.906 +v 53.775 -70.386 72.975 +v -90.552 -84.902 347.633 +v -70.568 -71.173 72.969 +v 85.402 -84.791 347.906 +v -70.306 -71.306 72.975 +v 85.402 -84.102 347.998 +v 53.714 -70.641 72.975 +v -70.198 -71.198 72.981 +v 52.202 -92 22.744 +v 51.764 -92 23.335 +v 53.623 -70.362 72.981 +v 51.386 -92 23.966 +v 53.568 -70.594 72.981 +v -90.122 -84.102 347.906 +v -70.966 87.503 61.957 +v -70.402 87.503 61.883 +v 52.697 -92 22.198 +v 53.435 -70.332 72.987 +v -70.227 -71.642 72.955 +v -70.451 -71.451 72.955 +v 53.386 -70.535 72.987 +v -90.669 -84.72 347.633 +v -70.214 -71.623 72.962 +v -70.435 -71.435 72.962 +v -90.754 -84.523 347.633 +v 87.092 -87.274 344.926 +v -70.173 -71.568 72.969 +v -90.113 -84.21 347.906 +v 51.886 -91.5 24.3468 +v -90.806 -84.315 347.633 +v -90.824 -84.102 347.633 +v 53.908 -70.704 72.955 +v 53.796 -70.976 72.955 +v 53.886 -70.697 72.962 +v 53.242 -92 21.704 +v 52.264 -91.5 23.7462 +v 53.775 -70.966 72.962 +v 52.702 -91.5 23.1826 +v 53.197 -91.5 22.6632 +v 53.821 -70.676 72.969 +v -93.7 -91.224 350.3 +v -93.673 -91.1446 350.995 +v -94.0001 -91.0153 350.995 +v -93.3145 -91.2272 350.995 +v -92.984 -91.389 350.3 +v 87.702 -86.302 345.815 +v -92.9705 -91.3066 350.995 +v 88.034 -86.619 344.926 +v -92.603 -91.3342 350.995 +v -92.25 -91.444 350.3 +v 53.714 -70.935 72.969 +v -92.25 -91.3606 350.995 +v 87.314 -86.619 345.815 +v 90.002 -62.837 66.03 +v -93.718 -91.277 348.873 +v 90.002 -62.961 66.946 +v 87.59 -86.982 344.926 +v 90.002 -63.002 67.884 +v -92.993 -91.444 348.873 +v 90.002 -59.627 61.883 +v 90.002 -60.155 61.957 +v 90.002 -60.67 62.176 +v 53.614 -70.883 72.975 +v 90.002 -61.16 62.537 +v 90.002 -61.611 63.029 +v 53.477 -71.107 72.975 +v 90.002 -62.014 63.641 +v 90.002 -62.358 64.357 +v 86.408 -87.061 345.815 +v 90.002 -62.635 65.16 +v 53.477 -70.814 72.981 +v 86.552 -87.488 344.926 +v 53.352 -71.017 72.981 +v -95.567 -90.128 350.3 +v -95.008 -90.586 350.3 +v -94.9566 -90.5186 350.995 +v -95.2257 -90.2984 350.995 +v -94.6421 -90.7029 350.995 +v 53.306 -70.727 72.987 +v -94.38 -90.954 350.3 +v -94.3406 -90.8801 350.995 +v -95.609 -90.167 348.873 +v 86.879 -86.875 345.815 +v -95.042 -90.631 348.873 +v 53.642 -71.227 72.955 +v 53.451 -71.451 72.955 +v -94.406 -91.004 348.873 +v 53.623 -71.214 72.962 +v 53.435 -71.435 72.962 +v 86.992 -86.195 346.575 +v 53.568 -71.173 72.969 +v 86.63 -86.407 346.575 +v 86.238 -86.562 346.575 +v 53.386 -71.386 72.969 +v 93 85.95 348.873 +v 93 71.961 296.886 +v 93 72.003 295.948 +v 53.306 -71.306 72.975 +v 93 71.837 297.802 +v 93 71.635 298.672 +v 93 71.358 299.475 +v 93 71.014 300.191 +v 93 70.611 300.803 +v 93 70.16 301.295 +v 93 69.67 301.656 +v 93 69.155 301.875 +v 93 68.627 301.949 +v 88.413 -86.195 344.926 +v 93 -63.002 295.948 +v 93 -62.961 296.886 +v 93 -62.837 297.802 +v 93 -62.635 298.672 +v 93 -62.358 299.475 +v 93 -62.014 300.191 +v 93 -61.611 300.803 +v 93 -61.16 301.295 +v 93 -60.67 301.656 +v 93 -60.155 301.875 +v 93 -59.627 301.949 +v 53.451 -70.125 70.013 +v 88.25 90.5 348.873 +v 53.642 -70.125 70.019 +v 70.003 90.5 295.948 +v 69.958 90.5 296.886 +v 69.826 90.5 297.802 +v 69.61 90.5 298.672 +v 69.315 90.5 299.475 +v 68.948 90.5 300.191 +v 68.518 90.5 300.803 +v 68.037 90.5 301.295 +v 67.515 90.5 301.656 +v 66.966 90.5 301.875 +v 66.402 90.5 301.949 +v 53.796 -70.125 70.025 +v 88.034 -85.931 345.815 +v 91.9756 88.5431 350.995 +v 92.046 88.592 350.3 +v 92.1603 88.254 350.995 +v 91.7358 88.8115 350.995 +v 91.567 89.128 350.3 +v 91.5056 89.0691 350.995 +v 91.008 89.586 350.3 +v 90.9566 89.5186 350.995 +v 53.908 -70.125 70.031 +v 91.2257 89.2984 350.995 +v -93.124 -86.689 340.547 +v 90.38 89.954 350.3 +v 90.6421 89.7029 350.995 +v 53.977 -70.125 70.038 +v -93.501 -86.1 340.547 +v 90.3406 89.8801 350.995 +v 86.879 -85.515 347.186 +v -92.655 -87.214 340.547 +v 86.35 -85.882 347.186 +v 53.775 -70.386 70.025 +v 92.7981 86.64 350.995 +v 92.8261 86.302 350.995 +v 92.884 86.653 350.3 +v 92.712 87.339 350.3 +v 92.6291 87.313 350.995 +v 92.712 86.9835 350.995 +v 53.886 -70.404 70.031 +v -93.451 -86.075 341.713 +v 92.4879 87.639 350.995 +v 92.43 87.99 350.3 +v 86.048 -86.002 347.186 +v 92.3526 87.9526 350.995 +v 53.954 -70.415 70.038 +v -92.615 -87.175 341.713 +v 86.408 -85.064 347.633 +v 92.093 88.624 348.873 +v 53.977 -70.418 70.045 +v 91.609 89.167 348.873 +v 86.238 -85.202 347.633 +v 86.63 -85.718 347.186 +v 91.042 89.631 348.873 +v -93.078 -86.657 341.713 +v 90.406 90.004 348.873 +v 86.048 -85.314 347.633 +v 53.435 -70.332 70.013 +v 92.942 86.662 348.873 +v 92.768 87.356 348.873 +v 53.386 -70.535 70.013 +v 92.482 88.016 348.873 +v -90.122 -88.448 340.547 +v 92.8551 85.95 350.995 +v 92.942 85.95 350.3 +v 53.623 -70.362 70.019 +v -90.113 -88.395 341.713 +v 53.568 -70.594 70.019 +v -90.824 -88.287 340.547 +v 89.673 90.1451 350.995 +v 90.0001 90.0153 350.995 +v 89.7 90.224 350.3 +v -70.966 90.5 301.875 +v 88.9705 90.3066 350.995 +v 88.984 90.389 350.3 +v -70.402 90.5 301.949 +v 89.3145 90.2277 350.995 +v -91.465 -87.975 341.713 +v 88.25 90.444 350.3 +v 88.603 90.3342 350.995 +v 53.714 -70.641 70.025 +v -91.491 -88.023 340.547 +v 88.25 90.3606 350.995 +v 89.718 90.277 348.873 +v -92.073 -87.618 341.713 +v 88.993 90.444 348.873 +v -92.106 -87.662 340.547 +v 87.59 -85.623 346.575 +v 87.314 -85.931 346.575 +v 53.821 -70.676 70.031 +v 85.402 83.791 347.906 +v 53.886 -70.697 70.038 +v 85.515 83.782 347.906 +v 87.263 -85.009 347.186 +v -90.806 -88.236 341.713 +v 85.625 83.757 347.906 +v 87.812 -85.277 346.575 +v 53.775 -70.966 70.038 +v 85.729 83.716 347.906 +v 53.908 -70.704 70.045 +v 85.825 83.659 347.906 +v 53.796 -70.976 70.045 +v 85.911 83.589 347.906 +v 85.985 83.507 347.906 +v 85.402 83.102 347.998 +v -92.496 -87.061 342.85 +v 53.306 -70.727 70.013 +v 85.402 85.689 346.575 +v 53.477 -70.814 70.019 +v 87.092 -85.277 347.186 +v 53.352 -71.017 70.019 +v 85.402 85.1 347.186 +v 85.729 85.075 347.186 +v 85.825 85.657 346.575 +v -92.301 -86.875 343.93 +v -92.718 -86.407 343.93 +v 53.614 -70.883 70.025 +v -92.942 -86.562 342.85 +v 85.402 84.462 347.633 +v 53.477 -71.107 70.025 +v 86.754 -84.523 347.633 +v 86.669 -84.72 347.633 +v 85.625 84.445 347.633 +v 86.552 -84.902 347.633 +v 85.842 84.396 347.633 +v 53.714 -70.935 70.031 +v -90.754 -88.082 342.85 +v 85.402 86.662 344.926 +v 85.402 86.214 345.815 +v 85.911 86.175 345.815 +v 85.985 86.618 344.926 +v -70.966 87.503 301.875 +v -70.402 87.503 301.949 +v -90.669 -87.831 343.93 +v -91.389 -87.831 342.85 +v 53.306 -71.306 70.025 +v -91.812 -87.274 343.93 +v 53.568 -71.173 70.031 +v -91.974 -87.488 342.85 +v 86.824 -84.102 347.633 +v 53.386 -71.386 70.031 +v 86.806 -84.315 347.633 +v 86.044 83.415 347.906 +v 86.087 83.315 347.906 +v 86.113 83.21 347.906 +v 86.806 83.315 347.633 +v 86.122 -84.102 347.906 +v 53.623 -71.214 70.038 +v 86.122 83.102 347.906 +v 86.113 -84.21 347.906 +v 86.824 83.102 347.633 +v 53.435 -71.435 70.038 +v 86.087 -84.315 347.906 +v -91.263 -87.596 343.93 +v 86.044 -84.415 347.906 +v 86.552 83.902 347.633 +v 53.642 -71.227 70.045 +v 53.451 -71.451 70.045 +v -93.946 -84.791 340.547 +v 87.974 -84.902 346.575 +v -94.002 -84.102 340.547 +v -93.946 -84.102 341.713 +v -93.723 -85.445 341.713 +v 53.227 -70.125 72.991 +v -93.777 -85.462 340.547 +v 86.669 83.72 347.633 +v 87.491 -84.102 347.186 +v 52.976 -70.125 72.995 +v 52.704 -70.125 72.998 +v 86.754 83.523 347.633 +v 87.974 83.902 346.575 +v 52.418 -70.125 72.999 +v -93.89 -84.782 341.713 +v 87.465 83.415 347.186 +v 53.214 -70.297 72.991 +v 87.491 83.102 347.186 +v 87.465 -84.415 347.186 +v 53.173 -70.466 72.991 +v 87.389 83.72 347.186 +v 53.107 -70.625 72.991 +v 87.389 -84.72 347.186 +v -93.563 -85.396 342.85 +v 52.966 -70.258 72.995 +v 89.078 83.659 344.926 +v -93.723 -84.757 342.85 +v 52.935 -70.388 72.995 +v 88.615 83.589 345.815 +v 52.883 -70.511 72.995 +v 89.124 83.102 344.926 +v -93.501 -84.102 343.93 +v -93.054 -85.882 343.93 +v 52.814 -70.625 72.995 +v -93.301 -86.002 342.85 +v 89.124 -84.102 344.926 +v 88.073 83.507 346.575 +v 89.078 -84.659 344.926 +v 88.106 83.102 346.575 +v 88.655 83.102 345.815 +v 87.59 85.982 344.926 +v 88.718 -85.718 344.926 +v -93.777 -84.102 342.85 +v 52.697 -70.216 72.998 +v 87.314 85.62 345.815 +v 88.034 85.62 344.926 +v 88.655 -84.102 345.815 +v 52.676 -70.304 72.998 +v 88.615 -84.589 345.815 +v 88.034 84.931 345.815 +v -93.301 -85.314 343.93 +v 88.942 -85.202 344.926 +v 52.641 -70.388 72.998 +v 52.415 -70.171 72.999 +v 86.879 85.875 345.815 +v -93.451 -84.716 343.93 +v 88.301 -85.515 345.815 +v 52.404 -70.216 72.999 +v 87.702 85.302 345.815 +v 52.386 -70.258 72.999 +v -89.402 -88.502 340.547 +v -89.402 -88.448 341.713 +v 52.362 -70.297 72.999 +v -89.402 -88.287 342.85 +v 52.332 -70.332 72.999 +v 86.879 84.515 347.186 +v -89.402 -88.023 343.93 +v 88.496 -85.064 345.815 +v 52.297 -70.362 72.999 +v -90.087 -88.236 342.85 +v 86.048 84.314 347.633 +v 33.002 15.003 350.995 +v 34.962 14.906 350.995 +v 86.35 84.882 347.186 +v 52.258 -70.386 72.999 +v 36.904 14.618 350.995 +v 38.808 14.141 350.995 +v 40.656 13.48 350.995 +v 42.43 12.641 350.995 +v 44.114 11.631 350.995 +v 45.69 10.462 350.995 +v 47.144 9.144 350.995 +v 86.63 84.719 347.186 +v 48.463 7.689 350.995 +v 49.632 6.113 350.995 +v 50.641 4.429 350.995 +v -90.044 -87.975 343.93 +v 33.002 15.003 347.998 +v 34.962 14.906 347.998 +v 36.904 14.618 347.998 +v 88.106 -84.102 346.575 +v 38.808 14.141 347.998 +v 40.656 13.48 347.998 +v 52.594 -70.466 72.998 +v 42.43 12.641 347.998 +v 86.238 84.202 347.633 +v 44.114 11.631 347.998 +v 45.69 10.462 347.998 +v 47.144 9.144 347.998 +v 52.535 -70.535 72.998 +v 86.408 84.064 347.633 +v 48.463 7.689 347.998 +v 49.632 6.113 347.998 +v 50.641 4.429 347.998 +v 52.466 -70.594 72.998 +v 87.314 84.931 346.575 +v 88.073 -84.507 346.575 +v 52.388 -70.641 72.998 +v 48.463 -17.689 350.995 +v 47.144 -19.144 350.995 +v 45.69 -20.462 350.995 +v 44.114 -21.631 350.995 +v 42.43 -22.641 350.995 +v 40.656 -23.48 350.995 +v 38.808 -24.141 350.995 +v 87.092 84.277 347.186 +v 36.904 -24.618 350.995 +v 87.59 84.623 346.575 +v 34.962 -24.906 350.995 +v 33.002 -25.003 350.995 +v 50.641 -14.429 350.995 +v 87.263 84.009 347.186 +v 49.632 -16.113 350.995 +v 87.812 84.277 346.575 +v -94.002 -62.961 296.886 +v -94.002 -63.002 295.948 +v 48.463 -17.689 347.998 +v -94.002 -62.837 297.802 +v 47.144 -19.144 347.998 +v 45.69 -20.462 347.998 +v -94.002 -62.635 298.672 +v 44.114 -21.631 347.998 +v -94.002 -62.358 299.475 +v 42.43 -22.641 347.998 +v -94.002 -62.014 300.191 +v 40.656 -23.48 347.998 +v 86.238 85.562 346.575 +v -94.002 -61.611 300.803 +v 38.808 -24.141 347.998 +v 52.216 -70.404 72.999 +v -94.002 -61.16 301.295 +v 36.904 -24.618 347.998 +v 86.992 85.195 346.575 +v -94.002 -60.67 301.656 +v 34.962 -24.906 347.998 +v 52.171 -70.415 72.999 +v 33.002 -25.003 347.998 +v -94.002 -60.155 301.875 +v 50.641 -14.429 347.998 +v -94.002 -59.627 301.949 +v 49.632 -16.113 347.998 +v 52.125 -70.418 72.999 +v 86.048 85.002 347.186 +v -74.003 -88.502 295.948 +v -73.958 -88.502 296.886 +v 86.63 85.407 346.575 +v -61.141 0.806 350.995 +v -60.48 2.655 350.995 +v -73.826 -88.502 297.802 +v -59.641 4.429 350.995 +v -58.632 6.113 350.995 +v -57.463 7.689 350.995 +v -73.61 -88.502 298.672 +v -56.144 9.144 350.995 +v -54.69 10.462 350.995 +v -53.114 11.631 350.995 +v -73.315 -88.502 299.475 +v -51.43 12.641 350.995 +v -49.656 13.48 350.995 +v -47.808 14.141 350.995 +v -72.948 -88.502 300.191 +v -45.904 14.618 350.995 +v -43.962 14.906 350.995 +v -42.002 15.003 350.995 +v -72.518 -88.502 300.803 +v -72.037 -88.502 301.295 +v 88.413 85.195 344.926 +v -71.515 -88.502 301.656 +v 53.198 -70.904 72.987 +v 88.718 84.719 344.926 +v -61.141 0.806 347.998 +v -60.48 2.655 347.998 +v 53.063 -71.063 72.987 +v -59.641 4.429 347.998 +v -58.632 6.113 347.998 +v -57.463 7.689 347.998 +v 53.017 -70.773 72.991 +v 88.301 84.515 345.815 +v -56.144 9.144 347.998 +v 88.942 84.202 344.926 +v -54.69 10.462 347.998 +v -53.114 11.631 347.998 +v 52.904 -70.904 72.991 +v -51.43 12.641 347.998 +v -49.656 13.48 347.998 +v -47.808 14.141 347.998 +v 52.773 -71.017 72.991 +v 88.496 84.064 345.815 +v -45.904 14.618 347.998 +v -43.962 14.906 347.998 +v -42.002 15.003 347.998 +v 87.092 86.274 344.926 +v -42.002 -25.003 350.995 +v -43.962 -24.906 350.995 +v -45.904 -24.618 350.995 +v -47.808 -24.141 350.995 +v -49.656 -23.48 350.995 +v -51.43 -22.641 350.995 +v -53.114 -21.631 350.995 +v -54.69 -20.462 350.995 +v 86.408 86.061 345.815 +v -56.144 -19.144 350.995 +v 86.552 86.488 344.926 +v -57.463 -17.689 350.995 +v -58.632 -16.113 350.995 +v -59.641 -14.429 350.995 +v -60.48 -12.655 350.995 +v -61.141 -10.806 350.995 +v -61.618 -8.902 350.995 +v -61.906 -6.961 350.995 +v -62.003 -5 350.995 +v 52.625 -71.107 72.991 +v -61.906 -3.039 350.995 +v 85.402 -88.502 340.547 +v -61.618 -1.098 350.995 +v 52.727 -70.727 72.995 +v -42.002 -25.003 347.998 +v -43.962 -24.906 347.998 +v 85.402 -88.287 342.85 +v -45.904 -24.618 347.998 +v 85.402 -88.448 341.713 +v -47.808 -24.141 347.998 +v 52.625 -70.814 72.995 +v -49.656 -23.48 347.998 +v -51.43 -22.641 347.998 +v -53.114 -21.631 347.998 +v 85.402 -88.023 343.93 +v -54.69 -20.462 347.998 +v 52.511 -70.883 72.995 +v -56.144 -19.144 347.998 +v -57.463 -17.689 347.998 +v 52.388 -70.935 72.995 +v -58.632 -16.113 347.998 +v 87.491 -88.023 340.547 +v -59.641 -14.429 347.998 +v 86.824 -88.287 340.547 +v -60.48 -12.655 347.998 +v 86.122 -88.448 340.547 +v 52.258 -70.966 72.995 +v -61.141 -10.806 347.998 +v -61.618 -8.902 347.998 +v -61.906 -6.961 347.998 +v -62.003 -5 347.998 +v -61.906 -3.039 347.998 +v 86.113 -88.395 341.713 +v -61.618 -1.098 347.998 +v 87.465 -87.975 341.713 +v 85.402 87.503 340.547 +v 85.402 87.448 341.713 +v 52.304 -70.676 72.998 +v 86.806 -88.236 341.713 +v 85.402 87.287 342.85 +v 85.402 87.023 343.93 +v 89.946 83.791 340.547 +v 90.002 83.102 340.547 +v 89.89 83.782 341.713 +v 89.946 83.102 341.713 +v -94.002 71.837 66.03 +v -94.002 71.961 66.946 +v 89.501 -86.1 340.547 +v 53.198 -71.198 72.981 +v 89.124 85.689 340.547 +v -94.002 72.003 67.884 +v 89.501 85.1 340.547 +v 53.017 -71.352 72.981 +v -73.826 87.503 66.03 +v 89.451 85.075 341.713 +v 88.106 -87.662 340.547 +v 89.723 84.445 341.713 +v -73.958 87.503 66.946 +v 89.777 84.462 340.547 +v 89.451 -86.075 341.713 +v -74.003 87.503 67.884 +v 89.078 85.657 341.713 +v 89.124 -86.689 340.547 +v 52.904 -71.198 72.987 +v 88.615 -87.175 341.713 +v 88.655 -87.214 340.547 +v 89.723 83.757 342.85 +v 88.073 -87.618 341.713 +v 89.451 83.716 343.93 +v 89.501 83.102 343.93 +v 89.777 83.102 342.85 +v 53.227 -71.642 72.955 +v 89.078 -86.657 341.713 +v 53.214 -71.623 72.962 +v 89.563 84.396 342.85 +v 88.301 85.875 343.93 +v 88.718 85.407 343.93 +v 88.942 85.562 342.85 +v 53.173 -71.568 72.969 +v 89.054 84.882 343.93 +v 89.301 85.002 342.85 +v 53.107 -71.477 72.975 +v 52.883 -71.614 72.975 +v 87.389 -87.831 342.85 +v -94.002 69.155 61.957 +v 86.754 -88.082 342.85 +v -94.002 69.67 62.176 +v 89.301 84.314 343.93 +v -94.002 70.16 62.537 +v 52.814 -71.477 72.981 +v -94.002 70.611 63.029 +v -94.002 71.014 63.641 +v 86.122 87.448 340.547 +v -94.002 71.358 64.357 +v 87.812 -87.274 343.93 +v 86.113 87.395 341.713 +v -94.002 71.635 65.16 +v 87.974 -87.488 342.85 +v 86.824 87.287 340.547 +v 87.465 86.975 341.713 +v 87.491 87.023 340.547 +v -71.515 87.503 62.176 +v -72.037 87.503 62.537 +v -72.518 87.503 63.029 +v 86.806 87.236 341.713 +v 86.669 -87.831 343.93 +v -72.948 87.503 63.641 +v -73.315 87.503 64.357 +v 86.087 -88.236 342.85 +v -73.61 87.503 65.16 +v 88.073 86.618 341.713 +v 88.106 86.662 340.547 +v 88.655 86.214 340.547 +v 87.263 -87.596 343.93 +v 88.615 86.175 341.713 +v 86.754 87.082 342.85 +v 86.044 -87.975 343.93 +v 86.044 86.975 343.93 +v 86.087 87.236 342.85 +v 86.669 86.831 343.93 +v 87.389 86.831 342.85 +v 87.812 86.274 343.93 +v 87.974 86.488 342.85 +v 88.942 -86.562 342.85 +v 88.496 -87.061 342.85 +v -92.655 83.102 52.175 +v 87.263 86.596 343.93 +v 88.718 -86.407 343.93 +v 88.496 86.061 342.85 +v -93.124 83.102 53.064 +v -93.078 83.659 53.064 +v -92.615 83.589 52.175 +v 88.301 -86.875 343.93 +v 90.002 -84.102 340.547 +v 89.946 -84.102 341.713 +v 89.946 -84.791 340.547 +v 89.723 -85.445 341.713 +v -91.491 83.102 50.805 +v 90.002 71.961 296.886 +v 90.002 72.003 295.948 +v 90.002 71.837 297.802 +v -91.465 83.415 50.805 +v 89.777 -85.462 340.547 +v 90.002 71.635 298.672 +v 90.002 71.358 299.475 +v 90.002 71.014 300.191 +v 90.002 70.611 300.803 +v 90.002 70.16 301.295 +v 90.002 69.67 301.656 +v 90.002 69.155 301.875 +v 90.002 68.627 301.949 +v 89.89 -84.782 341.713 +v 70.003 87.503 295.948 +v 69.958 87.503 296.886 +v 69.826 87.503 297.802 +v 69.61 87.503 298.672 +v 69.315 87.503 299.475 +v 68.948 87.503 300.191 +v 68.518 87.503 300.803 +v 68.037 87.503 301.295 +v 67.515 87.503 301.656 +v 66.966 87.503 301.875 +v -91.389 83.72 50.805 +v 66.402 87.503 301.949 +v 89.777 -84.102 342.85 +v 89.723 -84.757 342.85 +v -90.122 83.102 50.084 +v -90.824 83.102 50.357 +v -90.806 83.315 50.357 +v -90.113 83.21 50.084 +v 89.501 -84.102 343.93 +v -90.754 83.523 50.357 +v 89.301 -85.314 343.93 +v 89.563 -85.396 342.85 +v 89.054 -85.882 343.93 +v 89.301 -86.002 342.85 +v -90.552 83.902 50.357 +v 90.002 -63.002 295.948 +v 90.002 -62.961 296.886 +v 90.002 -62.837 297.802 +v 90.002 -62.635 298.672 +v 90.002 -62.358 299.475 +v 90.002 -62.014 300.191 +v 90.002 -61.611 300.803 +v 90.002 -61.16 301.295 +v 90.002 -60.67 301.656 +v 90.002 -60.155 301.875 +v 90.002 -59.627 301.949 +v 89.451 -84.716 343.93 +v -90.669 83.72 50.357 +v -92.106 83.102 51.415 +v -91.974 83.902 51.415 +v -92.073 83.507 51.415 +v 69.958 -88.502 296.886 +v 70.003 -88.502 295.948 +v 69.826 -88.502 297.802 +v 69.61 -88.502 298.672 +v 69.315 -88.502 299.475 +v 68.948 -88.502 300.191 +v -92.496 84.064 52.175 +v 68.518 -88.502 300.803 +v 68.037 -88.502 301.295 +v 67.515 -88.502 301.656 +v 66.966 -88.502 301.875 +v 66.402 -88.502 301.949 +v -92.942 84.202 53.064 +v -92.301 84.515 52.175 +v -92.413 85.195 53.064 +v -92.718 84.719 53.064 +v -90.238 84.202 50.357 +v -91.263 84.009 50.805 +v -90.408 84.064 50.357 +v 69.826 -88.502 66.03 +v 69.958 -88.502 66.946 +v -90.63 84.719 50.805 +v 70.003 -88.502 67.884 +v -90.35 84.882 50.805 +v -91.092 84.277 50.805 +v -90.879 84.515 50.805 +v 66.966 -88.502 61.957 +v 67.515 -88.502 62.176 +v 68.037 -88.502 62.537 +v 68.518 -88.502 63.029 +v 68.948 -88.502 63.641 +v 69.315 -88.502 64.357 +v 69.61 -88.502 65.16 +v -91.314 84.931 51.415 +v -91.812 84.277 51.415 +v -92.034 84.931 52.175 +v -91.59 84.623 51.415 +v 85.729 -86.075 50.805 +v -90.63 85.407 51.415 +v 85.402 -86.689 51.415 +v 85.402 -86.1 50.805 +v -91.702 85.302 52.175 +v 85.825 -86.657 51.415 +v -90.992 85.195 51.415 +v -90.879 85.875 52.175 +v 85.985 -87.618 53.064 +v -90.238 85.562 51.415 +v 85.911 -87.175 52.175 +v 85.402 -87.214 52.175 +v -91.314 85.62 52.175 +v 85.402 -87.662 53.064 +v -92.034 85.62 53.064 +v 85.842 -85.396 50.357 +v -91.59 85.982 53.064 +v 85.625 -85.445 50.357 +v 85.402 -85.462 50.357 +v -90.408 86.061 52.175 +v -91.092 86.274 53.064 +v -90.552 86.488 53.064 +v -93.501 83.102 54.06 +v -93.777 83.102 55.14 +v -93.723 83.757 55.14 +v -93.451 83.716 54.06 +v -93.301 84.314 54.06 +v -93.054 84.882 54.06 +v -93.563 84.396 55.14 +v -93.301 85.002 55.14 +v -92.942 85.562 55.14 +v -92.718 85.407 54.06 +v -92.301 85.875 54.06 +v -93.946 83.102 56.277 +v -93.89 83.782 56.277 +v -93.723 84.445 56.277 +v -93.078 85.657 56.277 +v -93.451 85.075 56.277 +v -91.812 86.274 54.06 +v -92.496 86.061 55.14 +v -91.389 86.831 55.14 +v -91.263 86.596 54.06 +v -90.754 87.082 55.14 +v -90.669 86.831 54.06 +v -91.974 86.488 55.14 +v -92.615 86.175 56.277 +v -92.073 86.618 56.277 +v -91.465 86.975 56.277 +v -90.806 87.236 56.277 +v -90.113 87.395 56.277 +v -90.087 83.315 50.084 +v -90.044 83.415 50.084 +v -89.911 83.589 50.084 +v -89.625 83.757 50.084 +v -89.985 83.507 50.084 +v -89.825 83.659 50.084 +v -89.729 83.716 50.084 +v -89.515 83.782 50.084 +v -89.402 83.791 50.084 +v -90.048 84.314 50.357 +v -89.625 84.445 50.357 +v -89.402 84.462 50.357 +v -90.048 85.002 50.805 +v -89.842 84.396 50.357 +v -89.729 85.075 50.805 +v -89.402 85.1 50.805 +# 3064 vertices + +g group_0_15277357 + +usemtl color_15277357 +s 0 + +f 20 38 1 +f 6 7 8 +f 3 7 6 +f 13 1 12 +f 16 1 15 +f 19 1 18 +f 19 20 1 +f 10 1 5 +f 10 12 1 +f 14 1 13 +f 14 15 1 +f 16 18 1 +f 4 5 1 +f 63 48 1 +f 7 3 24 +f 21 24 3 +f 9 26 17 +f 25 26 9 +f 26 67 17 +f 29 27 28 +f 30 27 31 +f 26 73 67 +f 32 27 33 +f 33 27 34 +f 17 67 64 +f 34 27 35 +f 36 35 27 +f 37 36 27 +f 38 37 27 +f 27 1 38 +f 22 39 25 +f 31 27 32 +f 30 28 27 +f 84 27 68 +f 4 1 48 +f 41 26 25 +f 39 41 25 +f 47 63 46 +f 41 73 26 +f 48 63 47 +f 44 49 40 +f 23 110 2 +f 53 63 52 +f 40 49 42 +f 55 63 54 +f 44 1745 59 +f 57 58 63 +f 63 58 46 +f 56 57 63 +f 1745 1752 59 +f 59 61 44 +f 51 52 63 +f 56 63 55 +f 53 54 63 +f 44 61 49 +f 3014 60 62 +f 51 63 71 +f 66 65 84 +f 29 68 27 +f 68 66 84 +f 2233 59 69 +f 76 75 84 +f 77 76 84 +f 79 77 84 +f 79 84 65 +f 69 2238 2233 +f 74 72 84 +f 72 70 84 +f 70 71 84 +f 63 84 71 +f 61 59 2233 +f 75 74 84 +f 94 87 92 +f 2 110 104 +f 11 107 99 +f 162 101 98 +f 85 106 100 +f 106 85 88 +f 106 108 100 +f 99 107 3017 +f 104 117 103 +f 100 108 102 +f 132 102 108 +f 104 110 117 +f 106 88 113 +f 81 78 114 +f 111 114 78 +f 93 113 88 +f 113 115 106 +f 113 123 115 +f 120 123 113 +f 126 112 109 +f 106 115 108 +f 113 93 120 +f 120 93 2851 +f 103 130 122 +f 103 117 130 +f 123 137 115 +f 1838 121 124 +f 2851 9 120 +f 9 17 120 +f 120 17 123 +f 17 64 123 +f 126 109 124 +f 123 64 137 +f 122 128 127 +f 112 126 129 +f 485 85 100 +f 100 102 485 +f 122 130 128 +f 102 132 131 +f 112 129 116 +f 116 129 91 +f 115 134 108 +f 108 134 132 +f 127 128 136 +f 115 137 134 +f 116 91 86 +f 139 2452 138 +f 121 140 124 +f 138 2452 2632 +f 176 2452 139 +f 121 235 140 +f 124 140 156 +f 140 185 156 +f 146 87 144 +f 96 87 95 +f 156 126 124 +f 146 1979 87 +f 1979 1980 87 +f 129 126 162 +f 90 92 87 +f 89 90 87 +f 94 95 87 +f 97 87 96 +f 97 144 87 +f 89 87 152 +f 194 152 87 +f 145 147 166 +f 149 194 148 +f 130 117 1326 +f 152 194 149 +f 158 194 155 +f 159 194 158 +f 161 194 159 +f 164 194 161 +f 126 156 162 +f 148 194 164 +f 153 155 194 +f 153 194 2028 +f 142 168 139 +f 142 139 138 +f 2650 2493 142 +f 142 2493 168 +f 169 187 173 +f 184 187 169 +f 171 143 172 +f 187 282 173 +f 177 2452 176 +f 178 2452 177 +f 129 98 91 +f 151 179 147 +f 98 129 162 +f 183 1883 182 +f 147 179 166 +f 179 184 166 +f 186 157 143 +f 181 182 1883 +f 157 186 154 +f 166 184 169 +f 180 1883 178 +f 2452 178 1883 +f 181 1883 180 +f 2656 1883 2655 +f 2656 2657 1883 +f 2658 2659 1883 +f 2661 2056 2660 +f 2665 2056 2663 +f 156 185 189 +f 185 193 189 +f 156 189 162 +f 186 143 171 +f 162 189 101 +f 174 173 282 +f 139 168 192 +f 139 192 176 +f 193 185 191 +f 189 105 101 +f 189 193 105 +f 184 179 2449 +f 191 197 193 +f 191 195 197 +f 193 199 105 +f 193 197 199 +f 150 154 220 +f 195 309 203 +f 176 192 204 +f 176 204 177 +f 195 203 197 +f 197 203 206 +f 145 166 205 +f 177 204 207 +f 177 207 178 +f 192 2493 204 +f 168 2493 192 +f 154 208 220 +f 197 206 199 +f 207 210 180 +f 207 180 178 +f 205 209 227 +f 208 154 186 +f 210 212 181 +f 210 181 180 +f 212 210 2272 +f 204 2493 207 +f 203 214 206 +f 203 211 214 +f 212 215 182 +f 212 182 181 +f 215 217 183 +f 215 183 182 +f 215 212 2272 +f 211 216 218 +f 215 2272 217 +f 211 218 214 +f 205 169 209 +f 166 169 205 +f 169 173 209 +f 201 196 200 +f 173 213 209 +f 150 220 198 +f 202 196 219 +f 219 196 221 +f 221 196 222 +f 223 222 196 +f 141 227 269 +f 225 223 196 +f 227 229 269 +f 220 208 563 +f 228 242 231 +f 202 200 196 +f 264 196 241 +f 224 235 121 +f 231 235 224 +f 201 241 196 +f 141 145 205 +f 239 237 264 +f 207 2272 210 +f 205 227 141 +f 227 209 229 +f 228 263 242 +f 231 242 245 +f 229 213 230 +f 242 247 245 +f 209 213 229 +f 231 245 235 +f 232 252 233 +f 246 140 235 +f 235 245 246 +f 246 185 140 +f 253 234 233 +f 271 247 242 +f 245 248 246 +f 245 247 248 +f 234 261 236 +f 246 248 191 +f 246 191 185 +f 230 250 232 +f 213 250 230 +f 247 249 251 +f 251 248 247 +f 241 239 264 +f 250 252 232 +f 244 1234 1419 +f 252 253 233 +f 251 305 309 +f 257 264 258 +f 258 264 259 +f 259 264 237 +f 253 261 234 +f 257 256 264 +f 256 255 264 +f 255 254 264 +f 261 307 236 +f 261 322 307 +f 265 266 262 +f 265 268 266 +f 267 268 265 +f 262 266 263 +f 173 174 213 +f 213 174 250 +f 271 242 263 +f 238 186 171 +f 186 238 208 +f 343 287 267 +f 268 267 287 +f 268 287 275 +f 230 269 229 +f 232 269 230 +f 263 266 271 +f 233 269 232 +f 234 269 233 +f 236 269 234 +f 271 266 272 +f 273 269 236 +f 271 249 247 +f 273 274 269 +f 272 249 271 +f 274 276 269 +f 272 275 277 +f 236 307 273 +f 275 279 277 +f 277 249 272 +f 273 429 274 +f 274 434 276 +f 429 434 274 +f 2860 2859 160 +f 448 160 281 +f 174 282 284 +f 266 268 272 +f 272 268 275 +f 187 288 282 +f 287 290 275 +f 289 290 287 +f 290 279 275 +f 282 291 284 +f 288 291 282 +f 284 291 349 +f 291 354 349 +f 292 297 289 +f 284 349 286 +f 289 297 290 +f 264 300 295 +f 290 283 279 +f 294 285 301 +f 299 300 264 +f 283 290 297 +f 300 302 295 +f 293 294 301 +f 300 312 302 +f 2499 350 288 +f 310 312 300 +f 742 278 303 +f 251 195 248 +f 295 302 296 +f 278 298 303 +f 251 309 195 +f 288 350 291 +f 195 191 248 +f 350 354 291 +f 305 251 249 +f 249 277 305 +f 306 308 299 +f 304 60 3014 +f 310 300 308 +f 299 308 300 +f 307 429 273 +f 250 311 252 +f 313 302 312 +f 327 304 3014 +f 313 329 302 +f 279 315 277 +f 250 174 311 +f 311 316 252 +f 312 310 399 +f 308 399 310 +f 306 1576 1506 +f 305 277 315 +f 305 315 318 +f 253 252 316 +f 253 319 261 +f 315 324 328 +f 301 143 157 +f 315 328 318 +f 319 253 316 +f 285 321 301 +f 305 318 309 +f 321 285 304 +f 309 211 203 +f 321 304 327 +f 309 318 211 +f 319 322 261 +f 301 321 143 +f 322 325 307 +f 351 296 302 +f 315 279 324 +f 322 403 325 +f 172 143 321 +f 324 279 283 +f 307 325 429 +f 284 311 174 +f 313 312 399 +f 329 313 399 +f 318 216 211 +f 311 286 316 +f 321 327 172 +f 328 216 318 +f 284 286 311 +f 316 334 319 +f 328 2137 216 +f 1528 1267 1275 +f 298 314 303 +f 334 316 286 +f 334 401 319 +f 314 317 303 +f 303 317 320 +f 296 351 331 +f 303 320 323 +f 319 401 322 +f 338 343 267 +f 326 303 323 +f 342 343 338 +f 330 303 326 +f 401 403 322 +f 330 332 303 +f 342 345 343 +f 344 345 342 +f 343 289 287 +f 345 289 343 +f 380 383 337 +f 344 348 345 +f 337 383 339 +f 347 348 344 +f 292 345 348 +f 446 346 293 +f 286 349 406 +f 345 292 289 +f 40 348 347 +f 42 348 40 +f 355 333 331 +f 349 362 406 +f 348 42 292 +f 2285 292 42 +f 354 362 349 +f 351 355 331 +f 355 356 333 +f 333 356 335 +f 11 359 353 +f 335 360 336 +f 356 360 335 +f 350 364 354 +f 302 329 351 +f 352 323 977 +f 365 351 329 +f 977 323 320 +f 365 366 351 +f 365 329 366 +f 363 11 367 +f 364 368 354 +f 352 326 323 +f 369 355 366 +f 351 366 355 +f 363 367 412 +f 371 356 372 +f 355 372 356 +f 369 372 355 +f 352 370 326 +f 354 368 362 +f 371 375 356 +f 376 360 375 +f 356 375 360 +f 370 357 330 +f 394 367 43 +f 360 376 377 +f 326 370 330 +f 357 361 332 +f 376 375 329 +f 376 329 377 +f 371 372 375 +f 372 366 375 +f 369 366 372 +f 329 375 366 +f 330 357 332 +f 360 380 336 +f 360 377 380 +f 336 380 337 +f 353 386 384 +f 391 393 380 +f 380 393 383 +f 386 353 359 +f 359 388 386 +f 339 389 341 +f 383 389 339 +f 388 359 363 +f 389 402 341 +f 412 388 363 +f 377 391 380 +f 382 387 415 +f 395 383 393 +f 397 389 396 +f 383 396 389 +f 367 394 412 +f 395 396 383 +f 382 415 385 +f 397 399 389 +f 294 43 45 +f 43 294 394 +f 377 329 391 +f 393 391 329 +f 395 393 329 +f 399 397 329 +f 396 329 397 +f 395 329 396 +f 1251 1554 1244 +f 341 402 63 +f 285 294 45 +f 334 407 401 +f 285 45 50 +f 389 399 402 +f 404 402 399 +f 285 50 304 +f 334 286 406 +f 420 384 386 +f 419 384 420 +f 51 71 448 +f 399 308 404 +f 406 407 334 +f 407 410 401 +f 401 410 403 +f 388 412 340 +f 410 440 403 +f 394 346 412 +f 406 362 416 +f 412 346 340 +f 346 394 293 +f 385 415 411 +f 416 407 406 +f 421 415 387 +f 293 394 294 +f 416 418 407 +f 421 422 415 +f 416 469 418 +f 385 411 408 +f 407 418 410 +f 400 421 387 +f 386 388 432 +f 415 422 411 +f 386 432 420 +f 388 340 432 +f 421 400 423 +f 65 424 79 +f 423 400 405 +f 66 425 424 +f 66 424 65 +f 423 426 421 +f 425 1406 424 +f 427 425 66 +f 427 66 68 +f 427 1406 425 +f 1204 133 1013 +f 1013 1210 1204 +f 421 426 422 +f 490 422 426 +f 428 430 149 +f 428 149 148 +f 149 430 433 +f 149 433 152 +f 428 1432 430 +f 423 405 431 +f 423 436 426 +f 431 436 423 +f 429 325 437 +f 325 439 437 +f 429 437 434 +f 419 420 479 +f 439 325 403 +f 440 439 403 +f 410 443 440 +f 442 420 432 +f 418 443 410 +f 432 340 457 +f 497 443 418 +f 433 430 1432 +f 457 442 432 +f 131 132 1743 +f 102 487 485 +f 368 364 445 +f 404 1506 402 +f 70 447 448 +f 70 448 71 +f 72 449 447 +f 72 447 70 +f 364 379 445 +f 472 368 445 +f 74 450 449 +f 74 449 72 +f 293 301 157 +f 450 1406 449 +f 441 357 370 +f 379 455 445 +f 75 451 450 +f 75 450 74 +f 269 276 1772 +f 76 453 451 +f 76 451 75 +f 157 446 293 +f 77 454 453 +f 77 453 76 +f 455 379 390 +f 454 1406 453 +f 79 456 454 +f 79 454 77 +f 441 452 357 +f 79 424 456 +f 452 361 357 +f 424 1406 456 +f 456 1406 454 +f 458 444 392 +f 455 480 445 +f 444 458 452 +f 431 438 436 +f 340 461 457 +f 455 390 460 +f 461 340 346 +f 346 446 461 +f 390 398 460 +f 85 460 398 +f 459 457 461 +f 85 485 460 +f 461 446 150 +f 1406 1202 160 +f 459 461 150 +f 368 468 362 +f 1548 1540 1244 +f 1406 160 448 +f 1406 448 447 +f 1406 447 449 +f 1406 450 451 +f 453 1406 451 +f 463 408 467 +f 150 446 154 +f 362 468 416 +f 408 411 467 +f 467 470 463 +f 154 446 157 +f 468 469 416 +f 198 459 150 +f 469 497 418 +f 463 470 465 +f 2906 845 911 +f 445 480 472 +f 465 470 778 +f 468 368 472 +f 474 467 411 +f 472 475 468 +f 411 422 474 +f 474 477 467 +f 479 420 442 +f 468 475 469 +f 474 484 477 +f 475 498 469 +f 467 477 470 +f 419 479 473 +f 479 442 481 +f 475 472 483 +f 480 483 472 +f 442 500 481 +f 460 485 455 +f 442 457 500 +f 484 490 496 +f 455 485 480 +f 486 488 155 +f 486 155 153 +f 496 784 484 +f 480 487 483 +f 459 500 457 +f 488 489 158 +f 488 158 155 +f 485 487 480 +f 474 422 490 +f 487 503 483 +f 489 491 159 +f 489 159 158 +f 473 492 478 +f 484 474 490 +f 491 493 161 +f 491 161 159 +f 492 473 479 +f 488 1432 489 +f 494 490 426 +f 493 495 164 +f 493 164 161 +f 481 492 479 +f 491 1432 493 +f 436 494 426 +f 507 478 492 +f 148 164 428 +f 495 428 164 +f 494 496 490 +f 102 131 487 +f 1432 428 495 +f 495 493 1432 +f 513 481 522 +f 498 497 469 +f 1698 73 80 +f 505 788 496 +f 475 502 498 +f 483 502 475 +f 522 481 500 +f 494 436 504 +f 503 502 483 +f 438 504 436 +f 2016 502 503 +f 496 494 505 +f 503 487 131 +f 504 505 494 +f 488 486 1432 +f 2128 1432 486 +f 507 506 478 +f 492 481 513 +f 492 513 507 +f 452 458 361 +f 2500 361 458 +f 2500 458 392 +f 459 280 500 +f 280 522 500 +f 459 198 280 +f 2016 498 502 +f 662 507 513 +f 2698 1530 1631 +f 516 392 444 +f 1866 1824 434 +f 510 523 509 +f 437 1913 434 +f 509 523 516 +f 516 523 392 +f 504 1061 505 +f 1466 419 473 +f 738 644 463 +f 738 463 740 +f 465 740 463 +f 537 198 220 +f 541 542 543 +f 563 537 220 +f 539 541 543 +f 539 543 540 +f 541 546 547 +f 541 547 542 +f 549 547 546 +f 196 553 539 +f 539 553 541 +f 541 554 546 +f 551 524 552 +f 553 554 541 +f 552 923 578 +f 537 562 555 +f 551 552 578 +f 562 537 563 +f 547 549 2715 +f 549 570 2715 +f 570 574 2715 +f 563 567 562 +f 549 546 570 +f 571 563 208 +f 572 521 524 +f 571 208 238 +f 521 572 545 +f 572 524 551 +f 548 571 238 +f 594 545 572 +f 563 571 567 +f 571 548 589 +f 542 547 2715 +f 574 582 2715 +f 570 546 573 +f 567 571 589 +f 594 572 602 +f 574 570 573 +f 589 548 118 +f 577 582 579 +f 118 548 119 +f 573 579 582 +f 573 582 574 +f 579 583 584 +f 579 584 577 +f 585 590 2715 +f 585 584 583 +f 586 590 587 +f 583 587 590 +f 583 590 585 +f 587 593 586 +f 594 588 545 +f 589 118 125 +f 631 589 125 +f 602 572 551 +f 522 280 601 +f 280 198 603 +f 598 551 578 +f 508 598 578 +f 198 537 603 +f 280 603 601 +f 577 584 2715 +f 582 577 2715 +f 584 585 2715 +f 590 586 2715 +f 586 593 2715 +f 593 605 2715 +f 587 604 593 +f 602 551 598 +f 1011 594 602 +f 593 604 605 +f 601 603 611 +f 603 537 555 +f 607 608 606 +f 602 598 976 +f 602 976 1011 +f 604 606 608 +f 604 608 605 +f 611 603 555 +f 606 609 610 +f 606 610 607 +f 601 611 600 +f 613 610 609 +f 609 646 614 +f 609 614 613 +f 2036 2777 2035 +f 562 567 627 +f 567 631 627 +f 1019 1030 1210 +f 567 589 631 +f 543 542 2715 +f 605 608 2715 +f 608 607 2715 +f 610 2715 607 +f 610 613 2715 +f 614 2715 613 +f 647 2715 614 +f 1228 1116 1157 +f 622 555 562 +f 621 573 546 +f 621 546 554 +f 621 624 573 +f 573 624 579 +f 622 794 791 +f 579 625 583 +f 627 622 562 +f 538 1272 626 +f 624 625 579 +f 622 627 794 +f 625 628 583 +f 626 673 538 +f 626 634 673 +f 632 634 626 +f 583 628 587 +f 2127 2027 836 +f 1019 1210 1013 +f 631 125 734 +f 634 632 385 +f 382 385 632 +f 587 636 604 +f 628 636 587 +f 408 644 385 +f 604 637 606 +f 636 637 604 +f 637 641 606 +f 600 642 640 +f 642 600 611 +f 641 609 606 +f 611 653 642 +f 641 1 609 +f 653 611 555 +f 1211 1210 1030 +f 686 687 1760 +f 645 692 640 +f 614 646 647 +f 609 1 646 +f 645 640 766 +f 408 463 644 +f 786 640 642 +f 1378 536 652 +f 789 642 653 +f 620 615 623 +f 622 653 555 +f 619 620 623 +f 653 622 791 +f 615 656 623 +f 928 658 615 +f 671 506 507 +f 506 671 659 +f 654 655 661 +f 615 658 656 +f 660 654 661 +f 662 671 507 +f 658 612 679 +f 513 663 662 +f 656 658 679 +f 663 513 522 +f 508 619 1135 +f 601 663 522 +f 1220 1062 1081 +f 659 669 668 +f 536 538 670 +f 538 673 670 +f 652 536 670 +f 669 659 671 +f 779 683 661 +f 671 662 703 +f 670 673 643 +f 673 644 643 +f 673 634 644 +f 634 385 644 +f 1211 1030 1044 +f 660 661 689 +f 656 679 1133 +f 661 683 689 +f 761 749 1680 +f 691 686 1760 +f 1760 1137 1131 +f 1131 1132 1760 +f 685 1369 651 +f 660 689 680 +f 669 671 703 +f 1760 1680 697 +f 1540 1241 1244 +f 692 662 663 +f 652 693 651 +f 703 662 692 +f 663 600 692 +f 691 1760 695 +f 685 651 693 +f 600 663 601 +f 708 683 919 +f 695 1760 697 +f 701 697 1680 +f 699 693 652 +f 680 704 688 +f 670 699 652 +f 689 704 680 +f 714 693 699 +f 669 703 716 +f 683 708 689 +f 640 692 600 +f 704 689 708 +f 643 699 670 +f 643 731 699 +f 699 731 714 +f 704 708 726 +f 713 668 669 +f 668 713 711 +f 669 716 713 +f 645 703 692 +f 714 775 693 +f 703 645 716 +f 688 721 719 +f 704 721 688 +f 704 726 721 +f 716 810 713 +f 919 730 708 +f 708 730 726 +f 718 714 733 +f 736 730 919 +f 731 733 714 +f 733 802 718 +f 644 738 643 +f 2191 101 105 +f 643 738 731 +f 740 733 731 +f 719 721 742 +f 739 719 742 +f 738 740 731 +f 1234 1157 1200 +f 700 701 1680 +f 684 776 741 +f 721 726 278 +f 278 742 721 +f 726 730 298 +f 750 752 1680 +f 744 700 1680 +f 278 726 298 +f 684 745 776 +f 730 736 314 +f 749 750 1680 +f 730 314 298 +f 747 744 1680 +f 752 747 1680 +f 736 317 314 +f 751 776 745 +f 317 736 971 +f 971 320 317 +f 762 810 716 +f 681 685 763 +f 762 716 645 +f 739 742 303 +f 645 766 762 +f 763 753 681 +f 763 770 753 +f 737 741 552 +f 761 1680 772 +f 753 770 756 +f 524 737 552 +f 770 796 756 +f 772 1680 774 +f 685 693 775 +f 775 763 685 +f 712 770 763 +f 712 763 775 +f 776 751 943 +f 714 718 775 +f 775 718 712 +f 78 740 465 +f 927 751 755 +f 78 465 778 +f 661 655 779 +f 477 781 470 +f 780 779 655 +f 470 781 778 +f 780 773 782 +f 477 783 781 +f 484 783 477 +f 779 780 782 +f 496 788 784 +f 484 784 783 +f 786 766 640 +f 789 786 642 +f 653 791 789 +f 505 1613 788 +f 1462 756 793 +f 756 796 793 +f 627 797 794 +f 798 796 770 +f 797 627 631 +f 770 712 798 +f 631 734 797 +f 712 718 799 +f 718 802 799 +f 1234 1200 1419 +f 799 798 712 +f 733 740 81 +f 773 803 782 +f 81 802 733 +f 785 803 773 +f 803 785 819 +f 78 81 740 +f 807 711 713 +f 806 711 807 +f 810 807 713 +f 805 816 815 +f 805 809 816 +f 808 819 785 +f 808 800 813 +f 2904 807 2911 +f 819 808 813 +f 800 838 813 +f 800 1040 838 +f 815 387 382 +f 815 816 387 +f 803 878 782 +f 818 809 817 +f 816 809 818 +f 816 818 400 +f 819 813 1010 +f 400 818 405 +f 816 400 387 +f 818 817 840 +f 877 830 837 +f 405 818 840 +f 828 842 848 +f 828 840 817 +f 755 837 928 +f 837 830 948 +f 1062 1220 1044 +f 928 837 948 +f 843 860 1156 +f 840 828 848 +f 852 1156 853 +f 830 845 920 +f 952 405 840 +f 840 848 952 +f 848 957 952 +f 805 1252 871 +f 870 876 871 +f 1540 1532 1241 +f 870 872 876 +f 871 876 809 +f 878 803 881 +f 1006 1013 133 +f 881 803 819 +f 871 809 805 +f 863 858 911 +f 855 1156 856 +f 857 856 1156 +f 86 2177 1783 +f 860 857 1156 +f 847 843 1156 +f 849 847 1156 +f 888 876 872 +f 855 853 1156 +f 850 1156 852 +f 851 1156 850 +f 751 745 869 +f 872 882 888 +f 888 817 876 +f 755 751 869 +f 892 930 891 +f 828 817 888 +f 817 809 876 +f 869 889 755 +f 900 930 899 +f 889 877 837 +f 777 1680 903 +f 902 930 901 +f 890 930 902 +f 891 930 890 +f 930 1156 893 +f 930 894 896 +f 930 896 897 +f 897 899 930 +f 932 1680 944 +f 774 1680 777 +f 900 901 930 +f 755 889 837 +f 863 904 1322 +f 894 930 893 +f 1322 904 877 +f 904 863 911 +f 917 888 882 +f 906 1161 905 +f 877 904 830 +f 907 1161 906 +f 828 888 917 +f 830 904 845 +f 914 1161 912 +f 1963 895 1947 +f 898 917 882 +f 918 1161 916 +f 915 1161 914 +f 1161 918 905 +f 911 845 904 +f 782 922 779 +f 923 552 741 +f 779 922 683 +f 924 917 898 +f 903 1680 925 +f 915 916 1161 +f 776 923 741 +f 2177 86 91 +f 910 912 1161 +f 909 910 1161 +f 908 909 1161 +f 924 898 921 +f 776 943 923 +f 917 924 842 +f 917 842 828 +f 755 928 927 +f 1216 929 921 +f 921 929 931 +f 1680 2027 944 +f 925 1680 932 +f 847 934 933 +f 847 933 843 +f 948 830 920 +f 849 935 934 +f 849 934 847 +f 935 2216 934 +f 921 931 924 +f 924 931 937 +f 2959 612 920 +f 2187 98 101 +f 938 939 906 +f 938 906 905 +f 906 939 941 +f 906 941 907 +f 942 578 923 +f 842 924 937 +f 943 751 927 +f 923 943 942 +f 922 782 878 +f 943 927 620 +f 1003 929 1000 +f 683 922 919 +f 931 929 1003 +f 927 928 615 +f 1532 1275 1288 +f 1532 1288 1241 +f 620 927 615 +f 937 963 954 +f 999 998 990 +f 878 881 958 +f 956 955 2027 +f 2187 91 98 +f 658 928 948 +f 612 948 920 +f 658 948 612 +f 405 952 431 +f 949 961 964 +f 954 848 842 +f 949 964 950 +f 842 937 954 +f 508 578 942 +f 950 967 951 +f 878 966 922 +f 970 930 951 +f 508 942 619 +f 943 620 942 +f 957 848 954 +f 619 942 620 +f 952 957 959 +f 958 966 878 +f 957 969 959 +f 1072 960 946 +f 431 952 959 +f 946 960 961 +f 959 438 431 +f 1080 974 960 +f 2959 920 845 +f 2898 679 612 +f 946 961 949 +f 441 962 1033 +f 954 963 965 +f 960 984 961 +f 957 954 965 +f 598 968 976 +f 922 966 919 +f 967 950 964 +f 508 968 598 +f 951 967 970 +f 969 957 965 +f 966 736 919 +f 1135 968 508 +f 966 958 971 +f 975 960 974 +f 736 966 971 +f 959 1048 438 +f 958 977 971 +f 881 978 958 +f 976 968 979 +f 973 976 979 +f 958 978 977 +f 983 895 982 +f 981 961 984 +f 960 975 984 +f 978 962 352 +f 961 981 990 +f 988 895 987 +f 989 895 988 +f 982 895 989 +f 965 1064 969 +f 977 978 352 +f 991 964 990 +f 961 990 964 +f 993 967 992 +f 964 992 967 +f 964 991 992 +f 441 370 962 +f 987 895 997 +f 999 970 998 +f 967 998 970 +f 352 962 370 +f 1000 929 1220 +f 967 993 998 +f 320 971 977 +f 1221 1000 1220 +f 1004 588 594 +f 939 2174 941 +f 938 2174 939 +f 931 1008 937 +f 1126 1121 1760 +f 931 1003 1008 +f 137 64 1713 +f 1011 1004 594 +f 1005 1013 1006 +f 64 67 1713 +f 1010 881 819 +f 1008 963 937 +f 1005 1007 1013 +f 1000 1016 1003 +f 1017 1033 838 +f 1007 1015 1019 +f 1021 895 1020 +f 1015 1030 1019 +f 1000 1014 1016 +f 1003 1016 1023 +f 1057 516 444 +f 1007 1019 1013 +f 895 1760 1020 +f 1026 1020 1760 +f 1057 444 1017 +f 978 881 1010 +f 1003 1023 1008 +f 1004 1024 588 +f 1008 1029 963 +f 1010 962 978 +f 813 1033 1010 +f 1008 1023 1029 +f 1030 1015 1027 +f 838 1033 813 +f 963 1034 965 +f 1024 1004 1041 +f 963 1029 1034 +f 1010 1033 962 +f 1034 1064 965 +f 1027 1038 1044 +f 1022 1024 1041 +f 1027 1044 1030 +f 1033 1017 441 +f 1014 1103 1016 +f 1052 1011 973 +f 973 1011 976 +f 441 1017 452 +f 850 1036 1037 +f 850 1037 851 +f 452 1017 444 +f 852 1039 1036 +f 852 1036 850 +f 853 1042 1039 +f 853 1039 852 +f 855 1043 1042 +f 855 1042 853 +f 1034 1122 1064 +f 1056 1038 1035 +f 1043 1047 2216 +f 856 1047 1043 +f 856 1043 855 +f 1038 1056 1062 +f 959 969 1048 +f 1038 1062 1044 +f 857 1051 1047 +f 857 1047 856 +f 1042 1043 2216 +f 1052 1004 1011 +f 860 1053 1051 +f 860 1051 857 +f 509 1058 1316 +f 67 1718 1713 +f 1041 1004 1052 +f 1047 1051 2216 +f 1048 1055 438 +f 1053 860 933 +f 843 933 860 +f 1048 1079 1055 +f 1040 1057 838 +f 933 2216 1053 +f 1051 1053 2216 +f 438 1055 504 +f 1084 1052 973 +f 1059 1060 909 +f 1059 909 908 +f 1056 1035 1054 +f 1040 1058 1057 +f 1060 1063 910 +f 1060 910 909 +f 1055 1061 504 +f 1059 2174 1060 +f 509 516 1058 +f 505 1061 1613 +f 984 999 981 +f 984 975 999 +f 974 999 975 +f 990 981 999 +f 972 999 974 +f 992 990 998 +f 991 990 992 +f 993 992 998 +f 1056 1081 1062 +f 1057 1058 516 +f 1017 838 1057 +f 503 2024 2016 +f 1064 1073 969 +f 1476 1091 1054 +f 1022 1076 1071 +f 1066 1075 1069 +f 1076 1022 1041 +f 1048 969 1073 +f 1074 1075 1066 +f 1069 1075 1080 +f 1056 1093 1081 +f 1041 1082 1076 +f 1069 1080 1072 +f 1073 1079 1048 +f 1082 1041 1052 +f 1079 1083 1055 +f 1072 1080 960 +f 1082 1052 1084 +f 1101 1117 1074 +f 1074 1085 1086 +f 1055 1083 1061 +f 979 1110 973 +f 1089 1075 1086 +f 1074 1086 1075 +f 303 1738 739 +f 973 1110 1084 +f 972 1080 1090 +f 1075 1090 1080 +f 1075 1089 1090 +f 1080 972 974 +f 1088 1104 1091 +f 1088 1102 1104 +f 1743 132 134 +f 1090 999 972 +f 1089 999 1090 +f 1086 999 1089 +f 1085 999 1086 +f 1753 134 137 +f 1054 1093 1056 +f 1054 1091 1093 +f 895 1099 1092 +f 1631 83 2698 +f 895 1098 1099 +f 1073 1154 1079 +f 1092 1099 1101 +f 1099 1112 1114 +f 1099 1114 1101 +f 1866 434 1913 +f 1100 1144 1146 +f 1092 1101 1094 +f 1074 1094 1101 +f 1382 1076 1082 +f 1082 1084 1383 +f 2011 497 498 +f 1094 1074 1066 +f 498 2016 2011 +f 1106 1023 1016 +f 1098 1105 1107 +f 1091 1104 1108 +f 1016 1103 1106 +f 1111 1029 1023 +f 1028 1026 1760 +f 1112 1099 1107 +f 1098 1107 1099 +f 1091 1108 1093 +f 1023 1106 1111 +f 1093 1108 1116 +f 1101 1114 1115 +f 1111 1119 1029 +f 1085 1074 1117 +f 1117 1101 1115 +f 1170 1111 1106 +f 979 1120 1110 +f 1117 999 1085 +f 1115 999 1117 +f 1114 999 1115 +f 1112 999 1114 +f 1107 999 1112 +f 1116 1081 1093 +f 1119 1034 1029 +f 1228 1081 1116 +f 1137 1760 687 +f 1063 1124 912 +f 1063 912 910 +f 1126 1760 1125 +f 1119 1122 1034 +f 1124 1127 914 +f 1124 914 912 +f 1102 1100 1146 +f 1063 2174 1124 +f 1127 1128 915 +f 1127 915 914 +f 1102 1146 1151 +f 1128 1130 916 +f 1128 916 915 +f 1125 1760 1132 +f 1151 1104 1102 +f 1133 679 2898 +f 1130 1134 918 +f 1130 918 916 +f 1130 1128 2174 +f 1127 2174 1128 +f 905 918 938 +f 1134 938 918 +f 2174 938 1134 +f 1134 1130 2174 +f 623 1145 619 +f 1164 1106 1103 +f 1136 1142 1138 +f 619 1145 1135 +f 1136 1139 1142 +f 1149 1145 623 +f 1175 1119 1111 +f 1138 1179 1177 +f 968 1135 1143 +f 968 1143 979 +f 1139 1152 1142 +f 1138 1142 1179 +f 933 934 2216 +f 1144 1187 1146 +f 656 1149 623 +f 1064 1122 1150 +f 1149 656 1133 +f 1122 1183 1150 +f 1145 1149 1147 +f 1064 1150 1073 +f 1150 1154 1073 +f 1155 1108 1104 +f 1139 1153 1152 +f 1104 1151 1155 +f 1157 1116 1108 +f 1079 1158 1083 +f 1108 1155 1157 +f 851 893 1156 +f 1154 1158 1079 +f 1135 1160 1143 +f 1145 1160 1135 +f 1145 1147 1160 +f 1122 1178 1183 +f 1150 1189 1154 +f 1173 1143 1160 +f 1183 1189 1150 +f 1633 1158 1154 +f 1191 1151 1146 +f 1148 1162 1153 +f 1153 1162 1152 +f 1133 1163 1149 +f 1151 1196 1155 +f 1164 1103 1237 +f 1149 1163 1147 +f 1166 1162 1148 +f 1155 1200 1157 +f 1159 1166 1148 +f 1164 1170 1106 +f 1159 1245 1166 +f 1143 1120 979 +f 1162 1166 1213 +f 1173 1120 1143 +f 1111 1170 1175 +f 1176 1140 1171 +f 1160 1147 1199 +f 1295 1213 1166 +f 1160 1199 1173 +f 1119 1178 1122 +f 1119 1175 1178 +f 1147 1202 1199 +f 1182 1181 1188 +f 1163 1202 1147 +f 1188 1181 1190 +f 1187 1144 1180 +f 1191 1146 1187 +f 1177 1179 1201 +f 1192 1177 1201 +f 1195 1181 1197 +f 1196 1151 1191 +f 1200 1155 1196 +f 1173 1199 1410 +f 1360 1181 1362 +f 1192 655 654 +f 1362 1181 1363 +f 1192 1201 655 +f 1201 1218 780 +f 1406 1199 1202 +f 1176 1171 1419 +f 133 1204 872 +f 1419 1180 1176 +f 655 1201 780 +f 1208 895 1207 +f 133 872 870 +f 1207 895 983 +f 1204 882 872 +f 1419 1200 1196 +f 1204 1210 882 +f 1226 1206 1209 +f 1210 1211 898 +f 1212 1206 1226 +f 1181 1214 1190 +f 1209 521 1226 +f 1214 1181 1195 +f 1210 898 882 +f 1211 921 898 +f 1179 1218 1201 +f 521 1209 1261 +f 1152 1219 1142 +f 1211 1216 921 +f 1142 1219 1179 +f 521 1261 524 +f 1216 1220 929 +f 1218 1179 1219 +f 1215 1419 1217 +f 1419 1171 1217 +f 1219 1222 1218 +f 1212 1224 1223 +f 1225 1219 1152 +f 1162 1225 1152 +f 1718 67 73 +f 1181 1227 1197 +f 1213 1225 1162 +f 1140 1176 1141 +f 1224 1212 1226 +f 1180 1144 1141 +f 1219 1225 1222 +f 545 1226 521 +f 1180 1141 1176 +f 1236 1225 1213 +f 1014 1000 1221 +f 1222 1225 1236 +f 545 1224 1226 +f 1228 1014 1221 +f 1968 439 440 +f 1213 1295 1239 +f 1228 1229 1232 +f 1231 1223 1224 +f 1236 1213 1239 +f 1230 1223 1231 +f 588 1224 545 +f 1188 1292 1182 +f 1228 1232 1014 +f 1224 588 1231 +f 1014 1232 1103 +f 1182 1292 1233 +f 1218 1222 773 +f 780 1218 773 +f 1229 1235 1232 +f 1222 785 773 +f 1005 1006 1260 +f 1229 1234 1235 +f 1232 1235 1237 +f 785 1222 1236 +f 1024 1231 588 +f 1237 1103 1232 +f 808 785 1236 +f 244 1235 1234 +f 1237 1235 244 +f 1239 808 1236 +f 808 1239 800 +f 1242 1244 1240 +f 382 632 815 +f 1240 1244 1241 +f 1159 1246 1245 +f 1242 1251 1244 +f 1206 1238 1209 +f 1248 1251 1242 +f 1319 1252 1247 +f 1245 1246 1269 +f 440 443 1981 +f 443 497 1981 +f 1295 1166 1245 +f 1209 1238 1254 +f 1245 1269 1309 +f 1256 1260 1248 +f 1295 1245 1309 +f 1254 1261 1209 +f 1248 1260 1251 +f 1776 1443 1256 +f 1005 1260 1256 +f 1261 1254 1264 +f 1246 1262 1269 +f 1253 1276 1255 +f 1264 1243 737 +f 1262 1263 1281 +f 1355 1270 1268 +f 1334 1257 1255 +f 1269 1262 1281 +f 1261 1264 737 +f 741 1243 684 +f 1257 1337 1258 +f 1270 1272 1268 +f 1334 1337 1257 +f 737 1243 741 +f 1265 1266 1274 +f 1342 1259 1258 +f 1268 1272 1299 +f 737 524 1261 +f 1266 1340 1274 +f 1318 1275 1267 +f 1340 1266 1330 +f 1270 1278 1272 +f 1318 1273 1275 +f 1270 1247 1278 +f 1190 1297 1188 +f 626 1272 1278 +f 1247 1252 1283 +f 1698 80 83 +f 1188 1297 1292 +f 1252 815 1283 +f 1280 1271 1787 +f 1271 1280 1285 +f 1283 1278 1247 +f 1586 1297 1190 +f 1278 1283 632 +f 1265 1287 1263 +f 1271 1285 1273 +f 1281 1263 1287 +f 632 1283 815 +f 1273 1288 1275 +f 1287 1265 1274 +f 1278 632 626 +f 1285 1288 1273 +f 1281 1287 1302 +f 1252 805 815 +f 1279 1254 1277 +f 1280 1240 1285 +f 1238 1277 1254 +f 1233 1291 1289 +f 1240 1280 1810 +f 1288 1285 1241 +f 1240 1241 1285 +f 1233 1292 1291 +f 1311 1294 1282 +f 1290 1299 1293 +f 1269 1300 1309 +f 1686 1292 1297 +f 1281 1300 1269 +f 1290 1268 1299 +f 745 1284 1286 +f 1302 1300 1281 +f 1297 1586 1606 +f 1293 1299 536 +f 1286 869 745 +f 1311 1304 1286 +f 1293 536 1378 +f 1348 1302 1287 +f 1286 1304 869 +f 1299 538 536 +f 1353 1303 1306 +f 1272 538 1299 +f 1307 1239 1295 +f 889 869 1304 +f 1295 1309 1307 +f 1296 1310 1298 +f 1913 437 439 +f 1291 1313 1289 +f 1309 1300 1324 +f 1289 1313 1305 +f 1308 1314 1355 +f 1313 1291 1710 +f 1312 1314 1308 +f 1277 1311 1279 +f 497 2004 1981 +f 1307 1309 1324 +f 1298 1310 1315 +f 1323 1305 1313 +f 1279 1311 1282 +f 1314 1270 1355 +f 1300 1302 1316 +f 1324 1300 1316 +f 1284 1294 1311 +f 1298 1315 1301 +f 1284 1311 1286 +f 1318 1303 1301 +f 1301 1315 1318 +f 1317 1319 1312 +f 1303 1267 1306 +f 1320 1304 1311 +f 1239 1307 800 +f 1303 1318 1267 +f 1320 1311 1322 +f 1307 1040 800 +f 1305 1323 1321 +f 1312 1319 1314 +f 1311 863 1322 +f 1314 1247 1270 +f 863 1311 858 +f 1319 1247 1314 +f 1307 1324 1040 +f 1320 889 1304 +f 1310 1271 1315 +f 1325 1317 1554 +f 1324 1316 1058 +f 1320 1322 877 +f 1321 1323 1327 +f 1315 1271 1273 +f 889 1320 877 +f 1326 1321 1327 +f 1325 1328 1317 +f 1315 1273 1318 +f 1040 1324 1058 +f 1317 1328 1319 +f 1345 1316 1302 +f 1264 1279 1282 +f 1254 1279 1264 +f 1316 1345 509 +f 1328 1252 1319 +f 1264 1282 1243 +f 1328 1325 871 +f 870 871 1325 +f 1243 1282 1294 +f 871 1252 1328 +f 1284 684 1294 +f 1728 1718 73 +f 684 1243 1294 +f 1255 1276 1334 +f 1348 1287 1274 +f 1284 745 684 +f 1331 1296 1336 +f 1339 1348 1274 +f 1337 1342 1258 +f 1327 1996 1326 +f 1336 1298 1341 +f 1339 1274 1340 +f 1332 1346 1335 +f 1343 1346 1332 +f 1338 1335 1290 +f 1350 1230 1231 +f 1230 1350 1349 +f 1345 1302 1348 +f 1346 1290 1335 +f 1290 1293 1338 +f 1348 1339 1352 +f 1344 2018 2115 +f 1347 1341 1301 +f 1345 1348 1352 +f 1358 1349 1350 +f 1354 1349 1358 +f 1308 1355 1343 +f 1345 1352 510 +f 1022 1358 1350 +f 1353 1356 1374 +f 1343 1355 1346 +f 510 509 1345 +f 1231 1024 1350 +f 1346 1268 1290 +f 1022 1350 1024 +f 1268 1346 1355 +f 1181 1359 1227 +f 1359 1181 1360 +f 1065 1354 1358 +f 73 1698 1728 +f 1071 1358 1022 +f 1698 83 1677 +f 83 1631 1677 +f 1428 1425 1136 +f 1336 1296 1298 +f 276 1824 1772 +f 1071 1065 1358 +f 1 27 646 +f 1301 1341 1298 +f 1357 1354 1065 +f 1303 1347 1301 +f 1347 1303 1353 +f 1359 1401 1227 +f 1338 1293 1366 +f 1306 1356 1353 +f 2131 654 660 +f 1367 1364 1361 +f 1361 1366 1367 +f 1359 1360 1386 +f 1367 1369 1364 +f 1364 1369 1365 +f 1369 681 1365 +f 1357 1065 1375 +f 1195 1590 1214 +f 1377 1065 1071 +f 1293 1378 1366 +f 331 624 621 +f 1065 1377 1375 +f 1331 1370 1368 +f 1367 1366 1378 +f 1379 1071 1076 +f 1367 651 1369 +f 1071 1379 1377 +f 1378 651 1367 +f 641 341 1 +f 1369 685 681 +f 1380 1197 1227 +f 1336 1371 1370 +f 1337 1399 1342 +f 1371 1336 1341 +f 1395 1399 1337 +f 1382 1379 1076 +f 1372 1371 1341 +f 1378 652 651 +f 1401 1380 1227 +f 1082 1383 1382 +f 1372 1341 1347 +f 1259 1342 1550 +f 1084 1385 1383 +f 1372 1347 1373 +f 1110 1385 1084 +f 1373 1347 1353 +f 1359 1386 1401 +f 1390 1276 1387 +f 1110 1389 1385 +f 1373 1353 1374 +f 1391 1334 1392 +f 1276 1392 1334 +f 1356 1376 1374 +f 1276 1390 1392 +f 1391 1394 1334 +f 1120 1389 1110 +f 1388 1417 1393 +f 1395 1337 1394 +f 1334 1394 1337 +f 1398 1427 1396 +f 1397 1342 1399 +f 1398 1368 1427 +f 1397 1565 1342 +f 1399 1558 1397 +f 1394 1558 1395 +f 1399 1395 1558 +f 1392 1558 1391 +f 1390 1558 1392 +f 1387 1558 1390 +f 1394 1391 1558 +f 1398 1329 1368 +f 1329 1398 2048 +f 1249 1403 1405 +f 1173 1407 1120 +f 1403 1455 1456 +f 1403 1456 1405 +f 1331 1368 1329 +f 1120 1407 1389 +f 1249 1405 1250 +f 1370 1331 1336 +f 1404 1408 1384 +f 1409 1411 5 +f 1409 5 4 +f 1410 1407 1173 +f 5 1411 1412 +f 5 1412 10 +f 1410 1199 1406 +f 1413 1360 1362 +f 1411 1136 1412 +f 1384 1408 1388 +f 10 1412 1415 +f 10 1415 12 +f 1415 1412 1136 +f 1360 1413 1386 +f 12 1415 1418 +f 12 1418 13 +f 1431 1386 1413 +f 13 1418 1420 +f 13 1420 14 +f 1419 1416 1414 +f 1417 1388 1408 +f 1250 1421 1253 +f 1215 1416 1419 +f 1250 1405 1421 +f 1361 1393 1417 +f 1422 1362 1363 +f 1413 1362 1422 +f 1418 1415 1136 +f 14 1420 1423 +f 14 1423 15 +f 1419 1187 1180 +f 1366 1361 1417 +f 1420 1418 1136 +f 1422 1450 1413 +f 1419 1191 1187 +f 1400 1393 1361 +f 15 1423 1424 +f 15 1424 16 +f 1419 1196 1191 +f 1423 1420 1136 +f 16 1424 1425 +f 16 1425 18 +f 1361 1364 1400 +f 1400 1364 1402 +f 18 1425 1428 +f 18 1428 19 +f 1426 1429 1427 +f 2004 497 2011 +f 19 1428 1430 +f 19 1430 20 +f 1364 1365 1402 +f 1396 1427 1429 +f 1424 1136 1425 +f 1370 1427 1368 +f 1408 1404 1335 +f 1371 1427 1370 +f 1372 1427 1371 +f 1428 1136 1430 +f 1373 1427 1372 +f 1375 1432 1357 +f 1332 1335 1404 +f 1374 1427 1373 +f 1431 1413 1450 +f 1335 1338 1408 +f 200 1433 1434 +f 200 1434 201 +f 1376 1427 1374 +f 202 1435 1433 +f 202 1433 200 +f 1338 1417 1408 +f 219 1436 1435 +f 219 1435 202 +f 1338 1366 1417 +f 221 1437 1436 +f 221 1436 219 +f 1429 2046 1396 +f 845 2906 2959 +f 1439 1401 1386 +f 222 1440 1437 +f 222 1437 221 +f 1402 1365 1438 +f 1440 1330 1437 +f 223 1442 1440 +f 223 1440 222 +f 612 2894 2898 +f 1439 1386 1431 +f 1442 1330 1440 +f 225 1445 1442 +f 225 1442 223 +f 226 1447 1445 +f 226 1445 225 +f 1436 1437 1330 +f 1441 1449 1443 +f 131 1743 1740 +f 1445 1330 1442 +f 1539 1453 1439 +f 1441 1446 1449 +f 1447 1330 1445 +f 1443 1449 1007 +f 2520 2521 1330 +f 2520 1330 1447 +f 1449 1015 1007 +f 1448 1462 1451 +f 1443 1007 1005 +f 1461 1462 1448 +f 3010 1223 1230 +f 1153 165 1148 +f 1449 1446 1452 +f 419 1466 1450 +f 1148 167 1159 +f 1463 1452 1446 +f 276 434 1824 +f 1421 1276 1253 +f 1449 1452 1015 +f 1452 1027 1015 +f 1455 1403 1454 +f 1438 1457 1444 +f 1458 1405 1456 +f 1460 1459 1421 +f 1466 1431 1450 +f 1405 1460 1421 +f 1452 1468 1027 +f 1457 1438 1365 +f 1405 1458 1460 +f 1457 1461 1444 +f 1421 1459 1387 +f 1460 1558 1459 +f 1421 1387 1276 +f 1444 1461 1448 +f 1481 1446 1441 +f 1459 1558 1387 +f 1456 1558 1458 +f 1455 1558 1456 +f 1454 1558 1455 +f 491 489 1432 +f 1559 1453 1539 +f 1439 1431 1539 +f 1156 1464 1403 +f 1675 1451 1462 +f 1431 1466 1539 +f 1365 681 1457 +f 1457 753 1461 +f 681 753 1457 +f 753 756 1461 +f 756 1462 1461 +f 1452 1463 1468 +f 1463 1467 1468 +f 1330 1434 1433 +f 1330 1433 1435 +f 1436 1330 1435 +f 137 1713 1753 +f 1424 1423 1136 +f 1411 1409 1136 +f 647 646 1784 +f 1465 1472 1467 +f 1465 1470 1472 +f 1468 1467 1035 +f 1472 1035 1467 +f 1468 1035 1038 +f 1468 1038 1027 +f 439 1952 1913 +f 1952 439 1968 +f 440 1981 1968 +f 1474 1438 1475 +f 1470 1476 1472 +f 1472 1054 1035 +f 1587 1388 1469 +f 1472 1476 1054 +f 63 402 1492 +f 84 63 1492 +f 1492 402 1506 +f 1469 1388 1393 +f 404 308 1506 +f 1469 1393 1471 +f 84 1478 1932 +f 1393 1400 1471 +f 1478 1480 1932 +f 1471 1400 1473 +f 1402 1474 1473 +f 1400 1402 1473 +f 1477 1484 1479 +f 1474 1402 1438 +f 1479 1484 1485 +f 1478 1497 1480 +f 1438 1444 1475 +f 1479 1485 1481 +f 1446 1481 1485 +f 1482 1500 1483 +f 1446 1485 1463 +f 1489 1491 1649 +f 1493 84 1492 +f 1492 1506 1507 +f 1485 1484 1495 +f 1492 1507 1493 +f 1484 1490 1495 +f 688 2242 680 +f 719 2297 688 +f 1471 1486 1581 +f 1495 1463 1485 +f 84 1493 1478 +f 1469 1471 1581 +f 1493 1497 1478 +f 1486 1473 1487 +f 2793 2791 1738 +f 1486 1471 1473 +f 1510 1511 1493 +f 1490 1465 1495 +f 1493 1511 1497 +f 1487 1474 1488 +f 1480 1498 1482 +f 1473 1474 1487 +f 1490 1496 1465 +f 1474 1475 1488 +f 1497 1498 1480 +f 1495 1465 1467 +f 1495 1467 1463 +f 1488 1475 1489 +f 1501 1489 1475 +f 1498 1500 1482 +f 1496 1591 1470 +f 1489 1501 1491 +f 1496 1470 1465 +f 1501 1503 1491 +f 1519 1523 1500 +f 1499 1504 1502 +f 1525 1572 1523 +f 1491 1503 1494 +f 1504 1499 1505 +f 1503 1508 1494 +f 1501 1448 1503 +f 1611 214 218 +f 1510 1493 1507 +f 1494 1508 1660 +f 1502 1504 1522 +f 1514 1497 1511 +f 1501 1475 1444 +f 1515 1498 1516 +f 1497 1516 1498 +f 1514 1516 1497 +f 2033 1504 1505 +f 1513 1476 1470 +f 1515 1517 1498 +f 1444 1448 1501 +f 1512 1521 1520 +f 1519 1500 1517 +f 1498 1517 1500 +f 1503 1448 1451 +f 1502 1522 1512 +f 1521 1512 1522 +f 1503 1451 1508 +f 1509 1526 1513 +f 1509 1524 1526 +f 1549 1546 1576 +f 1523 1519 1576 +f 1519 1517 1576 +f 1517 1515 1576 +f 1515 1516 1576 +f 1516 1514 1576 +f 1511 1576 1514 +f 1511 1510 1576 +f 1507 1576 1510 +f 1506 1576 1507 +f 1513 1526 1088 +f 1527 1528 1332 +f 2348 1564 1566 +f 1332 1528 1343 +f 1332 1404 1527 +f 1513 1088 1476 +f 1522 1531 1521 +f 1088 1091 1476 +f 1532 1343 1528 +f 1535 1524 1518 +f 1535 1518 1639 +f 1533 1538 1534 +f 1524 1535 1100 +f 1156 1403 1249 +f 1537 1538 1533 +f 1534 1538 1542 +f 1414 1529 1419 +f 1535 1144 1100 +f 1532 1540 1308 +f 1464 1541 1454 +f 1541 1558 1454 +f 1100 1526 1524 +f 1464 1454 1403 +f 1534 1542 194 +f 1458 1558 1460 +f 1539 1466 1543 +f 1308 1343 1532 +f 1526 1100 1102 +f 1526 1102 1088 +f 1540 1312 1308 +f 1549 1537 1546 +f 1550 1545 1259 +f 1548 1312 1540 +f 1552 1538 1551 +f 1537 1551 1538 +f 478 1543 1466 +f 1537 1549 1551 +f 1545 1555 1547 +f 1558 1542 1557 +f 1538 1557 1542 +f 1554 1317 1548 +f 1550 1555 1545 +f 1538 1552 1557 +f 1477 1704 1560 +f 1555 1561 1547 +f 1555 1575 1561 +f 1560 1484 1477 +f 1573 1575 1555 +f 1543 1559 1539 +f 1547 1561 1161 +f 1548 1317 1312 +f 1593 1594 1576 +f 1551 1549 1576 +f 1552 1551 1576 +f 2348 1953 1564 +f 1559 1543 1582 +f 1560 1563 1490 +f 1554 135 1325 +f 1568 1550 1565 +f 1565 1550 1342 +f 1568 1570 1550 +f 135 133 870 +f 135 870 1325 +f 1483 1500 1572 +f 1500 1523 1572 +f 1573 1555 1570 +f 1550 1570 1555 +f 1563 1496 1490 +f 1543 478 1582 +f 1576 1561 1575 +f 1560 1490 1484 +f 1564 1483 1572 +f 1564 1579 1566 +f 1486 1649 1581 +f 1564 1572 1579 +f 1559 1582 1556 +f 1570 1568 1558 +f 1565 1397 1558 +f 1568 1565 1558 +f 1566 1579 1584 +f 1487 1649 1486 +f 1496 1563 1580 +f 1579 1594 1584 +f 1582 1837 1556 +f 1488 1649 1487 +f 264 1161 299 +f 1576 306 1561 +f 1566 1584 1569 +f 1488 1489 1649 +f 1580 1591 1496 +f 1573 1558 1575 +f 1575 1558 1576 +f 1569 1537 1533 +f 930 1464 1156 +f 1584 1537 1569 +f 1529 1583 1530 +f 1574 1585 1587 +f 299 1561 306 +f 1214 1586 1190 +f 1589 1572 1525 +f 1525 1523 1576 +f 999 1541 1464 +f 1574 1587 1577 +f 1592 1579 1589 +f 1572 1589 1579 +f 1586 1214 1590 +f 1591 1580 1588 +f 930 970 1464 +f 1577 1469 1581 +f 970 999 1464 +f 1593 1584 1594 +f 1469 1577 1587 +f 1594 1579 1592 +f 1105 1541 1107 +f 1195 1197 1614 +f 1591 1588 1509 +f 999 1107 1541 +f 1584 1593 1546 +f 1573 1570 1558 +f 1590 1195 1614 +f 1594 1592 1576 +f 1553 1544 1530 +f 1618 1509 1588 +f 1584 1546 1537 +f 1562 1553 1530 +f 1380 1614 1197 +f 1591 1509 1513 +f 1384 1587 1585 +f 1530 1567 1562 +f 1592 1589 1576 +f 1589 1525 1576 +f 1546 1593 1576 +f 1470 1591 1513 +f 1384 1585 1597 +f 1530 1571 1567 +f 1530 1578 1571 +f 1587 1384 1388 +f 1583 1578 1530 +f 1267 1527 1306 +f 1686 1297 1606 +f 1597 1527 1404 +f 1600 1609 1588 +f 1597 1404 1384 +f 1600 1588 1580 +f 1061 1617 1613 +f 1083 1617 1061 +f 1609 1618 1588 +f 1609 1616 1618 +f 1618 1616 1518 +f 1626 1617 1083 +f 1158 1626 1083 +f 1618 1518 1524 +f 1524 1509 1618 +f 1189 1633 1154 +f 1158 1633 1636 +f 1590 1637 1586 +f 1586 1637 1606 +f 1158 1636 1626 +f 1634 1639 1518 +f 1634 1638 1639 +f 1635 1544 1687 +f 1530 1544 1635 +f 244 1164 1237 +f 1638 1140 1639 +f 1637 1590 1640 +f 244 1170 1164 +f 1640 1590 1614 +f 1687 1544 1610 +f 244 1175 1170 +f 1639 1140 1141 +f 1544 1553 1610 +f 244 1178 1175 +f 1639 1141 1535 +f 244 1183 1178 +f 244 1189 1183 +f 1553 1612 1610 +f 1535 1141 1144 +f 1553 1562 1612 +f 244 1633 1189 +f 244 1636 1633 +f 1612 1562 1615 +f 1615 1567 1619 +f 1562 1567 1615 +f 1577 1649 1574 +f 1651 1614 1380 +f 1648 1638 1696 +f 1581 1649 1577 +f 1614 1651 1640 +f 1567 1571 1619 +f 1651 1380 1653 +f 1653 1380 1401 +f 1652 1647 2149 +f 1530 1632 1631 +f 1647 1652 1654 +f 1439 1653 1401 +f 1530 1635 1632 +f 1656 1650 1655 +f 1494 1649 1491 +f 1647 1654 1648 +f 1649 1494 1660 +f 1657 1650 1658 +f 1658 1650 1659 +f 1692 1637 1640 +f 1660 1662 1649 +f 1659 1650 1661 +f 1648 1664 1638 +f 1663 1661 1650 +f 1648 1654 1664 +f 1665 1663 1650 +f 1701 1640 1651 +f 1140 1638 1664 +f 1660 1667 1662 +f 1660 1508 1667 +f 1668 1666 1650 +f 1215 1652 1416 +f 1666 1665 1650 +f 1508 1451 1671 +f 1669 1668 1650 +f 1652 1215 1654 +f 1654 1215 1217 +f 1650 1657 1655 +f 895 1650 1676 +f 1508 1671 1667 +f 1656 1676 1650 +f 1654 1217 1664 +f 1453 1653 1439 +f 1451 1675 1671 +f 1664 1217 1171 +f 1674 1673 895 +f 1171 1140 1664 +f 1676 1674 895 +f 1680 1760 1679 +f 1675 1462 793 +f 1520 1521 1691 +f 1292 1686 1291 +f 1531 1694 1521 +f 1635 1687 1681 +f 1521 1694 1691 +f 1606 1711 1686 +f 1689 1678 2138 +f 1531 2197 1688 +f 1670 1612 1672 +f 2138 2141 1689 +f 1678 1689 1690 +f 1610 1612 1670 +f 1711 1606 1726 +f 1612 1615 1672 +f 1637 1726 1606 +f 1536 1705 1703 +f 1678 1690 1684 +f 1677 1632 1681 +f 1520 1691 1536 +f 1690 1616 1684 +f 1705 1536 1691 +f 1631 1632 1677 +f 1616 1609 1684 +f 1632 1635 1681 +f 1701 1692 1640 +f 2141 1693 1689 +f 1695 1651 1653 +f 1694 1531 1688 +f 1689 1693 1696 +f 1701 1651 1695 +f 1694 1707 1691 +f 1653 1453 1695 +f 1648 1696 1693 +f 1690 1689 1696 +f 1694 1688 1709 +f 1616 1690 1634 +f 1707 1694 1709 +f 1696 1634 1690 +f 1634 1518 1616 +f 1699 1692 1701 +f 1647 1693 2146 +f 1647 1648 1693 +f 1702 1610 1670 +f 1687 1610 1702 +f 1696 1638 1634 +f 1670 1672 1697 +f 1705 1691 1707 +f 1677 1681 1698 +f 1706 1560 1704 +f 358 1636 244 +f 1698 1681 1700 +f 1681 1687 1700 +f 1700 1687 1702 +f 1686 1710 1291 +f 1706 1708 1563 +f 1708 1580 1563 +f 1710 1686 1711 +f 1688 1712 1709 +f 1706 1563 1560 +f 1600 1708 1731 +f 1600 1580 1708 +f 1713 1719 1714 +f 1718 1719 1713 +f 1719 1722 1714 +f 1710 1711 1723 +f 1720 1725 1721 +f 1714 1722 1716 +f 1724 1725 1720 +f 1726 1637 1692 +f 1725 1706 1721 +f 1738 1709 1712 +f 1721 1706 1704 +f 1712 1715 1738 +f 1726 1692 1727 +f 1719 1718 1730 +f 1724 1731 1725 +f 1728 1730 1718 +f 1699 1727 1692 +f 1730 1733 1719 +f 1729 1731 1724 +f 1730 1702 1733 +f 1731 1708 1725 +f 1739 1731 1729 +f 1711 1735 1723 +f 1722 1719 1733 +f 1726 1735 1711 +f 1726 1727 1735 +f 1725 1708 1706 +f 1733 1697 1722 +f 1735 1742 1723 +f 2557 1617 1626 +f 1700 1730 1728 +f 1698 1700 1728 +f 1737 1739 1729 +f 1738 1703 1705 +f 1738 1705 1707 +f 1757 1727 1792 +f 1700 1702 1730 +f 1738 1707 1709 +f 1702 1670 1733 +f 1733 1670 1697 +f 1600 1731 1739 +f 1717 1738 1715 +f 1762 1601 1611 +f 1741 40 347 +f 1743 1745 1740 +f 1738 1717 1732 +f 1735 1727 1757 +f 1743 1752 1745 +f 1732 1734 1738 +f 1735 1757 1742 +f 1740 1745 1741 +f 1738 1734 2294 +f 1741 44 40 +f 1739 1737 1684 +f 1745 44 1741 +f 1750 1743 134 +f 1678 1684 1737 +f 1792 1727 1699 +f 1750 1752 1743 +f 1739 1684 1609 +f 1762 1611 218 +f 1739 1609 1600 +f 2133 2132 1721 +f 134 1753 1750 +f 1753 1754 1750 +f 1755 1744 895 +f 1756 1755 895 +f 1756 895 1673 +f 1750 1754 1752 +f 1742 1757 1860 +f 1754 69 1752 +f 1685 1760 1744 +f 1683 1760 1685 +f 1679 1760 1682 +f 1682 1760 1683 +f 895 1744 1760 +f 69 59 1752 +f 1754 1753 1714 +f 1713 1714 1753 +f 1766 1695 1453 +f 1754 1716 69 +f 1714 1716 1754 +f 1773 1776 1763 +f 1763 1776 1771 +f 1761 1248 1242 +f 1771 1248 1761 +f 1793 1794 1772 +f 1775 1701 1695 +f 1794 1795 1772 +f 1785 1784 646 +f 1699 1701 1775 +f 1695 1766 1775 +f 1699 1775 1800 +f 1809 1812 2753 +f 2309 2753 2347 +f 1771 1256 1248 +f 1775 1766 1779 +f 1780 1781 1782 +f 1783 1772 1801 +f 1776 1256 1771 +f 1781 1784 1785 +f 1781 1785 1782 +f 1801 86 1783 +f 1773 1441 1776 +f 1443 1776 1441 +f 1782 1786 1780 +f 1775 1779 1800 +f 1005 1256 1443 +f 1779 1766 1556 +f 1453 1559 1766 +f 1556 1766 1559 +f 1310 1787 1271 +f 1772 1824 1825 +f 1121 1113 1760 +f 1280 1787 1791 +f 1795 1796 1772 +f 1796 1797 1772 +f 1797 1799 1772 +f 1012 1002 895 +f 1881 895 1208 +f 1800 1792 1699 +f 1799 1801 1772 +f 1831 1794 1793 +f 1792 1800 1868 +f 1807 1789 1803 +f 1833 1795 1794 +f 1781 1780 2753 +f 1780 1786 2753 +f 1786 1809 2753 +f 1807 1791 1789 +f 1786 1808 1809 +f 1556 1841 1779 +f 1791 1807 1810 +f 1782 1808 1786 +f 1808 1811 1812 +f 1808 1812 1809 +f 1710 1813 1313 +f 109 1797 1796 +f 1313 1813 1323 +f 1723 1813 1710 +f 1280 1791 1810 +f 1797 109 112 +f 2343 2753 1822 +f 1799 1797 112 +f 1826 1323 1813 +f 1814 1812 1811 +f 1826 1327 1323 +f 1815 1816 1817 +f 1819 1826 1813 +f 1811 1817 1816 +f 1811 1816 1814 +f 1799 112 116 +f 1799 116 1801 +f 1807 1803 1821 +f 1813 1723 1819 +f 1817 1823 1815 +f 86 1801 116 +f 1742 1819 1723 +f 1821 1761 1807 +f 1782 1935 1808 +f 1772 1825 1827 +f 1827 1793 1772 +f 1671 147 1667 +f 1761 1810 1807 +f 1826 1819 2002 +f 1810 1761 1242 +f 895 1828 1829 +f 895 1829 1961 +f 1113 1028 1760 +f 1810 1242 1240 +f 1851 1843 895 +f 1827 1830 1793 +f 1861 1862 895 +f 1819 1742 2015 +f 1763 1821 1840 +f 1830 1831 1793 +f 1863 1851 895 +f 1763 1771 1821 +f 2015 1742 1860 +f 1821 1771 1761 +f 895 1021 1012 +f 895 1002 996 +f 997 895 996 +f 1831 1833 1794 +f 1837 1841 1556 +f 1795 1833 1838 +f 1477 1479 1888 +f 1861 895 1870 +f 1850 1893 1481 +f 1441 1773 1481 +f 1481 1773 1850 +f 1795 1838 1796 +f 109 1796 1838 +f 1821 1803 1840 +f 2375 1842 892 +f 1830 1890 1831 +f 1841 1837 1852 +f 1846 1763 1840 +f 1839 1841 1852 +f 224 1833 1831 +f 1890 224 1831 +f 1877 1870 895 +f 224 121 1833 +f 1843 1828 895 +f 1876 1850 1846 +f 1833 121 1838 +f 1846 1773 1763 +f 1838 124 109 +f 1846 1850 1773 +f 1863 895 1862 +f 1757 1864 1860 +f 1864 1757 1792 +f 1866 1867 1824 +f 1868 1864 1792 +f 1866 1915 1867 +f 1859 1865 1840 +f 1867 1825 1824 +f 1825 1873 1827 +f 1800 1779 1892 +f 1867 1873 1825 +f 1846 1840 1865 +f 1873 1830 1827 +f 1803 1859 1840 +f 1875 1871 1872 +f 1877 895 1882 +f 1878 1875 1872 +f 1878 1872 1879 +f 1882 895 1881 +f 1892 1779 1841 +f 1867 1917 1873 +f 1883 1878 1879 +f 1865 1876 1846 +f 1884 1860 1864 +f 1873 1917 1885 +f 1873 1885 1830 +f 1885 1890 1830 +f 1883 2659 1878 +f 1985 1888 1893 +f 1800 1892 1868 +f 1894 2003 1895 +f 1903 1871 1895 +f 1893 1850 1876 +f 1903 1895 1897 +f 1905 1903 1897 +f 1905 1897 1906 +f 1893 1888 1479 +f 2415 2410 802 +f 1839 1892 1841 +f 1479 1481 1893 +f 1897 1970 1906 +f 1890 1885 228 +f 1907 1905 1906 +f 2133 1721 1704 +f 228 231 1890 +f 1890 231 224 +f 1909 1911 1908 +f 1914 1864 1868 +f 1911 1907 1906 +f 1906 1908 1911 +f 1864 1914 1884 +f 1913 1915 1866 +f 1872 1871 1908 +f 1909 1908 1871 +f 145 1662 1667 +f 1915 1917 1867 +f 2025 1916 1921 +f 1868 1922 1914 +f 1922 1868 1892 +f 1916 1936 1921 +f 1922 1892 1839 +f 1812 1814 2753 +f 2342 2753 2343 +f 1816 2753 1814 +f 1816 1815 2753 +f 1823 2753 1815 +f 1822 2753 1823 +f 1785 1932 1782 +f 1912 1931 1296 +f 1912 1921 1931 +f 1932 1785 27 +f 1931 1310 1296 +f 1885 1917 1934 +f 2410 798 799 +f 228 1885 1934 +f 1921 1936 1943 +f 1934 263 228 +f 1884 1914 1976 +f 895 1958 1955 +f 1932 1935 1782 +f 1921 1943 1931 +f 1914 1950 1976 +f 1935 1951 1808 +f 1931 1787 1310 +f 1480 1951 1935 +f 262 263 1934 +f 1950 1914 1922 +f 1667 147 145 +f 1931 1943 1787 +f 1946 1947 895 +f 1808 1951 1811 +f 1951 1953 1811 +f 1952 1954 1913 +f 1789 1943 1936 +f 1897 1967 1970 +f 1966 1789 1936 +f 1961 1958 895 +f 1943 1791 1787 +f 1943 1789 1791 +f 1954 1915 1913 +f 1477 1888 1704 +f 1954 1959 1915 +f 1969 1669 1962 +f 1915 1959 1917 +f 81 114 802 +f 1895 1871 1894 +f 1905 1871 1903 +f 1907 1911 1905 +f 1871 1905 1911 +f 2001 1894 1871 +f 1871 1911 1909 +f 1959 1934 1917 +f 1962 1650 87 +f 1968 1971 1952 +f 1969 1972 1669 +f 1650 1962 1669 +f 1980 1669 1972 +f 2204 1965 2206 +f 1906 1973 1908 +f 1972 1974 1980 +f 1968 1984 1971 +f 1976 1965 1884 +f 1962 87 1974 +f 1952 1971 1954 +f 1906 1970 1973 +f 1977 1959 1954 +f 1978 1908 1973 +f 87 1980 1974 +f 1965 1976 2206 +f 1971 1977 1954 +f 1872 1908 1978 +f 1960 1975 1859 +f 1977 262 1959 +f 1872 1978 1842 +f 1975 1865 1859 +f 1976 1950 2208 +f 262 1977 265 +f 1960 1859 1966 +f 1872 1842 1879 +f 262 1934 1959 +f 1966 1803 1789 +f 2208 1950 2213 +f 796 2388 793 +f 1966 1859 1803 +f 1922 1839 1982 +f 955 953 2027 +f 1950 1922 1982 +f 1981 1984 1968 +f 1983 1975 2017 +f 2213 1950 1982 +f 2415 802 114 +f 947 944 2027 +f 1865 1975 1983 +f 1971 1987 1977 +f 1839 1986 1982 +f 1984 1987 1971 +f 1986 1839 1852 +f 1983 1876 1865 +f 1983 1985 1876 +f 1987 265 1977 +f 1982 1986 2216 +f 1811 1953 1817 +f 1888 1985 2101 +f 1326 1996 1333 +f 1953 2348 1817 +f 1985 1893 1876 +f 1993 1994 1992 +f 1987 267 265 +f 1994 1991 1990 +f 1990 1992 1994 +f 2001 1993 1992 +f 2001 1992 2003 +f 338 267 1987 +f 1785 646 27 +f 799 802 2410 +f 2004 2006 1981 +f 2004 2014 2006 +f 2011 2014 2004 +f 2003 1894 2001 +f 2089 2027 2090 +f 2005 1936 1916 +f 1897 1895 2003 +f 2006 1984 1981 +f 2006 338 1984 +f 2006 342 338 +f 2014 342 2006 +f 1990 2008 2010 +f 1984 338 1987 +f 1997 2007 1960 +f 2007 1975 1960 +f 1990 2010 1992 +f 147 1671 1675 +f 2005 1997 1960 +f 2015 2002 1819 +f 895 2228 194 +f 2005 1960 1966 +f 1966 1936 2005 +f 2018 1333 1996 +f 2016 2019 2011 +f 2013 2104 2117 +f 1333 2018 1344 +f 1996 2020 2018 +f 1975 2007 2017 +f 1327 2021 1996 +f 2011 2019 2014 +f 1826 2021 1327 +f 2014 2019 344 +f 2017 2099 1983 +f 194 1999 2027 +f 2002 2021 1826 +f 1996 2021 2020 +f 2014 344 342 +f 2016 2026 2019 +f 2016 2024 2026 +f 2166 194 2295 +f 2028 194 2027 +f 2026 347 2019 +f 1992 2012 2003 +f 2002 2015 2045 +f 1912 1331 1329 +f 2525 2029 2042 +f 1329 2025 1912 +f 2019 347 344 +f 1331 1912 1296 +f 2015 2030 2045 +f 2026 2024 1741 +f 1741 2024 1740 +f 2556 1613 1617 +f 1741 347 2026 +f 1860 2030 2015 +f 1884 2030 1860 +f 2022 2031 1916 +f 2061 2608 2036 +f 2777 2036 2608 +f 2033 1505 2034 +f 2031 2005 1916 +f 2610 2775 2608 +f 2034 2023 2055 +f 1916 2025 2022 +f 2002 2039 2021 +f 2021 2039 2020 +f 2025 1921 1912 +f 2033 2034 2055 +f 2039 2002 2045 +f 1502 2144 1499 +f 2023 2041 2055 +f 1512 2142 1502 +f 1662 141 1649 +f 2031 1997 2005 +f 1512 1520 2142 +f 2041 2023 2042 +f 1675 151 147 +f 2065 2042 2029 +f 2044 1429 1426 +f 793 2386 1675 +f 1992 2010 2012 +f 2041 2042 2065 +f 2046 1429 2044 +f 2012 1967 2003 +f 2048 1398 1396 +f 1396 2046 2048 +f 1962 2036 2035 +f 2013 2151 2029 +f 2003 1967 1897 +f 2065 2029 2151 +f 2047 2066 2049 +f 2973 1669 2097 +f 1980 2097 1669 +f 2047 2064 2066 +f 2035 2973 2051 +f 2097 2051 2973 +f 2044 2053 2046 +f 2071 1504 2033 +f 1962 2035 1969 +f 2051 1969 2035 +f 2050 2053 2044 +f 2046 2053 2022 +f 87 1650 2316 +f 1994 1993 1871 +f 2001 1871 1993 +f 2053 2031 2022 +f 953 947 2027 +f 2053 2070 2031 +f 2046 2022 2048 +f 2056 2666 2318 +f 2067 956 2027 +f 788 2252 784 +f 2048 2022 2025 +f 2552 2252 1613 +f 2084 2075 2027 +f 2087 2085 2027 +f 2055 2041 2095 +f 2025 1329 2048 +f 2061 2062 2608 +f 2045 2030 2111 +f 2036 1962 2061 +f 2070 2053 2050 +f 1965 2030 1884 +f 2049 2043 2040 +f 2030 1965 2111 +f 2049 2066 2043 +f 2065 2151 2182 +f 2085 2084 2027 +f 2060 1578 2064 +f 2020 2069 2018 +f 2050 2068 2070 +f 2060 1571 1578 +f 1504 2071 1522 +f 2018 2069 2115 +f 2064 1583 2066 +f 2058 2059 2073 +f 2064 1578 1583 +f 2033 2094 2071 +f 2059 2056 2057 +f 2059 2057 2073 +f 2066 1583 1529 +f 2097 2074 2051 +f 2066 1529 2043 +f 2077 2079 2076 +f 2097 2062 2074 +f 2094 2033 2055 +f 2250 2244 784 +f 1969 2051 1972 +f 2074 1972 2051 +f 1972 2062 1974 +f 2074 2062 1972 +f 2061 1974 2062 +f 2079 2058 2073 +f 2076 2079 2073 +f 2081 2070 2068 +f 1962 1974 2061 +f 2082 2077 2076 +f 2082 2076 1990 +f 2055 2095 2094 +f 2056 2318 2316 +f 2075 2067 2027 +f 2081 2068 2078 +f 2070 2081 1997 +f 2087 2027 2089 +f 1991 2082 1990 +f 2316 1650 2057 +f 2552 1613 2556 +f 2057 1650 2088 +f 2031 2070 1997 +f 2057 2088 2073 +f 2073 2091 2076 +f 1531 1522 2071 +f 2073 2088 2091 +f 2076 2008 1990 +f 2093 2081 2078 +f 2008 2076 2091 +f 2086 2047 2080 +f 2078 2092 2093 +f 2047 2049 2126 +f 2081 2007 1997 +f 2081 2093 2007 +f 2094 2197 2071 +f 1536 1703 2270 +f 2270 1703 1738 +f 1871 1875 2059 +f 2113 2093 2092 +f 2093 2017 2007 +f 146 2096 1979 +f 2059 2058 1871 +f 2058 2079 1871 +f 2079 2077 1871 +f 2077 2082 1871 +f 2082 1991 1871 +f 1994 1871 1991 +f 1619 2060 2083 +f 2017 2093 2113 +f 2095 2214 2094 +f 1979 2096 2097 +f 1979 2097 1980 +f 2096 2607 2097 +f 2017 2113 2099 +f 2086 2083 2060 +f 2060 2064 2086 +f 895 194 1098 +f 1542 1098 194 +f 151 1675 2386 +f 2119 2101 2099 +f 1542 1105 1098 +f 2086 2064 2047 +f 1680 836 2027 +f 1619 1571 2060 +f 2099 1985 1983 +f 2099 2101 1985 +f 2129 2103 2101 +f 1888 2101 2103 +f 2102 2114 2105 +f 2112 2114 2102 +f 2020 2107 2069 +f 1704 1888 2103 +f 2039 2107 2020 +f 2103 2133 1704 +f 2039 2108 2107 +f 2105 2032 2160 +f 2045 2108 2039 +f 2250 784 2252 +f 2127 836 734 +f 2102 2215 2112 +f 2113 2092 2110 +f 2111 2108 2045 +f 2110 2119 2113 +f 2110 2118 2119 +f 1105 1558 1541 +f 11 1311 107 +f 2131 660 2145 +f 1351 1344 2115 +f 2114 2037 2105 +f 2113 2119 2099 +f 2105 2037 2032 +f 2115 2069 2224 +f 2388 796 798 +f 2136 2104 2121 +f 2126 2221 2080 +f 2127 2128 2028 +f 2127 2028 2027 +f 2118 2124 2129 +f 2121 2109 2125 +f 2028 2128 486 +f 2028 486 153 +f 2136 2121 2125 +f 2124 2133 2129 +f 2112 2130 2114 +f 2118 2129 2119 +f 2126 2130 2112 +f 3002 1238 1206 +f 2119 2129 2101 +f 2114 2130 2038 +f 3002 1206 1212 +f 2130 2049 2040 +f 2204 2231 2111 +f 2145 2109 2131 +f 2130 2040 2038 +f 119 1230 1349 +f 2204 2111 1965 +f 2114 2038 2037 +f 119 1349 1354 +f 2133 2124 2132 +f 2080 2047 2126 +f 734 1357 1432 +f 2104 2136 2117 +f 2098 2100 2144 +f 2126 2049 2130 +f 2133 2103 2129 +f 2127 734 1432 +f 2128 2127 1432 +f 1105 1542 1558 +f 1720 1721 2132 +f 2137 1762 218 +f 2137 218 216 +f 7 2139 8 +f 2090 2027 2140 +f 2140 2027 1999 +f 8 2139 2137 +f 2137 2143 1762 +f 2139 2143 2137 +f 2109 2145 2125 +f 2098 2144 2106 +f 7 24 2148 +f 2141 2146 1693 +f 2142 2106 2144 +f 2125 2145 2234 +f 2148 2139 7 +f 2148 2150 2139 +f 2146 2149 1647 +f 1652 2149 2152 +f 2151 2013 2117 +f 194 2159 2154 +f 1999 194 2154 +f 2148 2162 2150 +f 2139 2150 2143 +f 1617 2557 2556 +f 2155 2106 2142 +f 2156 24 21 +f 1652 2152 1416 +f 2159 194 2158 +f 2157 2158 194 +f 1416 2152 1414 +f 2160 2148 24 +f 793 2388 2386 +f 2160 24 2156 +f 2155 2142 2169 +f 2160 2162 2148 +f 2157 194 2164 +f 2163 2164 194 +f 2161 1724 1601 +f 2153 2155 2169 +f 2235 2117 2136 +f 2163 194 2165 +f 1601 1724 1720 +f 2102 2105 2156 +f 2161 1729 1724 +f 2156 2105 2160 +f 2167 1729 2161 +f 2168 1737 2167 +f 2032 2162 2160 +f 2041 2170 2095 +f 2167 1737 1729 +f 2170 2041 2065 +f 1520 1536 2169 +f 1678 1737 2168 +f 788 1613 2252 +f 2138 1678 2168 +f 2173 2246 1697 +f 1536 2270 2189 +f 1601 1720 2132 +f 2044 1426 2177 +f 2170 2065 2182 +f 2175 2180 2171 +f 2175 2083 2180 +f 2177 2050 2044 +f 2177 2185 2050 +f 2182 2151 2188 +f 2117 2188 2151 +f 2185 2187 2068 +f 1697 1672 2173 +f 2117 2235 2188 +f 2153 2189 2184 +f 2182 2188 2186 +f 2173 2190 2175 +f 2557 1626 1636 +f 2185 2068 2050 +f 2189 2153 2169 +f 2173 1672 2190 +f 2190 2083 2175 +f 2187 2191 2078 +f 2078 2191 2092 +f 2256 2186 2188 +f 2078 2068 2187 +f 2083 2086 2180 +f 2243 778 781 +f 2180 2086 2080 +f 783 2244 781 +f 2071 2197 1531 +f 2191 2196 2092 +f 2190 1672 1615 +f 2190 1619 2083 +f 1615 1619 2190 +f 2094 2214 2197 +f 2195 2184 2266 +f 21 3 2259 +f 2170 2199 2095 +f 2214 2095 2199 +f 2110 2092 2196 +f 2196 2203 2110 +f 2201 194 2217 +f 2203 2118 2110 +f 2203 2207 2118 +f 2205 2156 21 +f 2206 1976 2208 +f 2197 2211 1688 +f 111 78 778 +f 2207 2212 2124 +f 2200 2215 2205 +f 2243 781 2244 +f 2210 2215 2200 +f 783 784 2244 +f 2207 2124 2118 +f 2205 2102 2156 +f 2216 2213 1982 +f 2211 2197 2214 +f 1955 1946 895 +f 2215 2102 2205 +f 2212 2132 2124 +f 269 1649 141 +f 2222 194 2225 +f 145 141 1662 +f 1611 2132 2212 +f 1611 1601 2132 +f 2170 2220 2199 +f 2182 2220 2170 +f 2219 2221 2210 +f 2182 2186 2220 +f 206 2207 199 +f 1351 2223 2174 +f 2199 2220 2268 +f 2115 2223 1351 +f 2210 2221 2215 +f 798 2400 2388 +f 2186 2288 2220 +f 2115 2224 2223 +f 798 2410 2400 +f 2221 2112 2215 +f 2226 194 2228 +f 2107 2227 2069 +f 1964 895 1963 +f 2228 895 1964 +f 2268 2220 2288 +f 2143 2161 1601 +f 2219 2171 2180 +f 2225 194 2226 +f 2069 2227 2224 +f 2167 2161 2143 +f 1712 1688 2211 +f 2180 2080 2219 +f 2108 2229 2107 +f 2219 2080 2221 +f 2211 1715 1712 +f 2107 2229 2227 +f 2199 2230 2214 +f 2111 2231 2108 +f 2214 2230 2211 +f 2270 2184 2189 +f 2221 2126 2112 +f 2230 2199 2268 +f 2266 2184 2270 +f 2211 2230 1715 +f 2229 2108 2231 +f 2207 214 2212 +f 2557 1636 2560 +f 1611 2212 214 +f 2560 1636 358 +f 2233 2236 61 +f 2125 2247 2136 +f 111 2237 2260 +f 2136 2247 2235 +f 69 1716 2238 +f 2234 2247 2125 +f 111 2260 2258 +f 2238 2241 2233 +f 778 2237 111 +f 2238 2249 2241 +f 2233 2241 2236 +f 778 2243 2237 +f 1716 1722 2246 +f 2024 503 131 +f 1772 1426 1427 +f 2238 1716 2246 +f 131 1740 2024 +f 2235 2247 2245 +f 2246 2249 2238 +f 2234 2242 2247 +f 2249 2171 2241 +f 2245 2247 2242 +f 1722 1697 2246 +f 254 2251 264 +f 2250 2252 2284 +f 2246 2173 2249 +f 1161 264 2253 +f 2253 264 2251 +f 2173 2175 2249 +f 2249 2175 2171 +f 2235 2256 2188 +f 1561 299 1161 +f 2256 2235 2245 +f 2256 2245 2296 +f 2259 3 2257 +f 2262 2260 2237 +f 2237 2243 2262 +f 2257 3 6 +f 2243 2244 2267 +f 2195 2266 2264 +f 2267 2262 2243 +f 2269 21 2259 +f 2277 2267 2244 +f 2269 2265 2200 +f 2186 2291 2288 +f 2244 2250 2277 +f 2265 2271 2200 +f 2277 2279 2267 +f 2200 2205 2269 +f 2267 2273 2262 +f 2269 2205 21 +f 2543 2272 2541 +f 2274 2271 2236 +f 2232 2272 2543 +f 2232 2254 2272 +f 2236 2241 2274 +f 2174 260 136 +f 2254 2255 2272 +f 2267 2279 2273 +f 2271 2274 2210 +f 2255 2261 2272 +f 2272 2261 2264 +f 2251 2281 2282 +f 2251 2282 2253 +f 2272 2264 2266 +f 254 2281 2251 +f 2271 2210 2200 +f 2272 2266 2270 +f 2282 2281 260 +f 2281 2870 260 +f 1059 2253 2282 +f 1717 2230 2268 +f 2241 2171 2274 +f 2171 2219 2274 +f 2284 2277 2250 +f 3060 1233 1289 +f 2274 2219 2210 +f 1717 1715 2230 +f 23 1289 1305 +f 2207 206 214 +f 2268 1732 1717 +f 1305 110 23 +f 2284 2287 2277 +f 1326 117 110 +f 1333 130 1326 +f 2289 2285 42 +f 128 130 1333 +f 42 49 2289 +f 2288 1732 2268 +f 2285 2290 2286 +f 2279 2277 2287 +f 2285 2289 2290 +f 2174 1059 260 +f 2282 260 1059 +f 2286 2290 2259 +f 2288 1734 1732 +f 2256 2291 2186 +f 308 306 1506 +f 2286 2259 2257 +f 49 61 2293 +f 1557 1552 1576 +f 2288 2291 1734 +f 1127 1124 2174 +f 1060 2174 1063 +f 2293 2289 49 +f 2293 2265 2289 +f 2294 1734 2291 +f 2566 2287 2284 +f 270 1311 1181 +f 1557 1576 1558 +f 2291 2256 2296 +f 2271 2265 2293 +f 2295 194 2302 +f 2165 194 2166 +f 2304 2302 194 +f 2202 2305 194 +f 1037 1036 2216 +f 1036 1039 2216 +f 1039 1042 2216 +f 2291 2296 2294 +f 2265 2290 2289 +f 2290 2269 2259 +f 2296 2280 2294 +f 2488 2438 2298 +f 2265 2269 2290 +f 2217 194 2222 +f 2293 61 2236 +f 2242 2297 2245 +f 2296 2245 2297 +f 2271 2293 2236 +f 2296 2297 2280 +f 2300 2298 2258 +f 2258 2260 2300 +f 2300 2303 2298 +f 2437 2450 1255 +f 1255 1257 2437 +f 2294 2280 1738 +f 2202 194 2201 +f 2285 2286 292 +f 297 292 2286 +f 2298 2303 2299 +f 297 2286 283 +f 2257 283 2286 +f 1192 654 2131 +f 1545 2472 1259 +f 324 283 6 +f 2304 194 2305 +f 2257 6 283 +f 328 324 8 +f 6 8 324 +f 2145 660 2234 +f 680 2234 660 +f 2306 2300 2260 +f 8 2137 328 +f 2242 2234 680 +f 2260 2262 2306 +f 2242 688 2297 +f 739 2280 2297 +f 719 739 2297 +f 739 1738 2280 +f 2308 2309 2307 +f 893 851 1037 +f 2310 2308 2307 +f 2310 2307 2311 +f 384 1422 1363 +f 2306 2262 2273 +f 2307 2325 2311 +f 2751 2752 2318 +f 2313 2310 2311 +f 2315 2313 2311 +f 2315 2311 2316 +f 1852 668 1986 +f 711 806 1986 +f 2300 2306 2317 +f 2318 2315 2316 +f 2300 2317 2303 +f 2314 2319 2312 +f 890 2320 2322 +f 890 2322 891 +f 891 2322 2324 +f 891 2324 892 +f 2320 806 2322 +f 2317 2332 2357 +f 2329 2332 2317 +f 105 2196 2191 +f 2324 2322 806 +f 105 199 2196 +f 2325 2307 2321 +f 2326 2327 894 +f 2326 894 893 +f 2314 2323 2319 +f 2326 806 2327 +f 894 2327 2328 +f 894 2328 896 +f 2327 806 2328 +f 2273 2329 2306 +f 896 2328 2330 +f 896 2330 897 +f 2311 87 2316 +f 1601 1762 2143 +f 2323 2292 2349 +f 897 2330 2331 +f 897 2331 899 +f 2311 2325 87 +f 2306 2329 2317 +f 2328 806 2330 +f 899 2331 2333 +f 899 2333 900 +f 2319 2323 2349 +f 2330 806 2331 +f 900 2333 2334 +f 900 2334 901 +f 2333 2331 806 +f 2292 2335 2349 +f 901 2334 2336 +f 901 2336 902 +f 2038 2040 2146 +f 2279 2337 2273 +f 2334 2333 806 +f 902 2336 2320 +f 902 2320 890 +f 2292 2882 2338 +f 2320 2336 806 +f 2334 806 2336 +f 2273 2337 2329 +f 2752 2753 2318 +f 2750 2751 2318 +f 2749 2750 2318 +f 1037 2326 893 +f 2335 2292 2338 +f 2337 2339 2329 +f 2340 1822 1823 +f 1823 1817 2340 +f 2098 2106 2338 +f 1422 384 1450 +f 2329 2339 2332 +f 2342 2343 2341 +f 2338 2106 2335 +f 1582 478 506 +f 1837 1582 506 +f 2341 2343 2340 +f 1822 2340 2343 +f 2344 2342 2341 +f 2344 2341 2346 +f 2287 2345 2279 +f 711 1986 668 +f 2216 1986 806 +f 2341 2353 2346 +f 2337 2279 2345 +f 1037 2216 2326 +f 806 2326 2216 +f 2347 2344 2346 +f 2337 2725 2339 +f 2307 2309 2346 +f 2347 2346 2309 +f 2032 2138 2162 +f 2345 2725 2337 +f 1817 2348 2340 +f 2725 2729 2339 +f 419 1450 384 +f 1466 473 478 +f 2340 2348 2350 +f 2345 2733 2725 +f 659 1837 506 +f 2341 2340 2350 +f 659 668 1837 +f 1852 1837 668 +f 2299 2354 2351 +f 2353 2341 2350 +f 2303 2354 2299 +f 2353 2321 2346 +f 2351 2355 2352 +f 1363 1181 353 +f 11 353 1181 +f 2167 2143 2150 +f 1363 353 384 +f 2150 2162 2167 +f 2354 2355 2351 +f 2346 2321 2307 +f 2032 2037 2138 +f 2356 2312 2319 +f 2149 2040 2152 +f 2303 2317 2357 +f 2319 2358 2356 +f 2358 2319 2349 +f 2303 2357 2354 +f 101 2191 2187 +f 2344 2753 2342 +f 2347 2753 2344 +f 2357 2360 2354 +f 2349 2335 2359 +f 2358 2349 2359 +f 2203 199 2207 +f 2360 2355 2354 +f 2155 2335 2106 +f 1772 1783 1426 +f 1529 1414 2043 +f 2355 2360 2847 +f 2335 2155 2359 +f 1530 1419 1529 +f 2420 2356 2358 +f 2168 2167 2162 +f 2357 2332 2364 +f 2363 2361 2362 +f 2168 2162 2138 +f 2350 2348 1566 +f 2141 2138 2037 +f 2365 2361 2366 +f 2357 2364 2360 +f 2037 2038 2141 +f 2146 2141 2038 +f 1566 1569 2350 +f 2366 2361 2367 +f 2149 2146 2040 +f 2368 2367 2361 +f 2043 2152 2040 +f 2369 2368 2361 +f 2152 2043 1414 +f 2370 2369 2361 +f 2332 2817 2364 +f 1783 2177 1426 +f 91 2185 2177 +f 2371 2361 2372 +f 2185 91 2187 +f 2372 2361 2373 +f 2153 2359 2155 +f 2817 2821 2364 +f 2203 2196 199 +f 2371 2370 2361 +f 2365 2362 2361 +f 2488 2298 2299 +f 2377 1842 2376 +f 2381 1842 2380 +f 2382 1842 2381 +f 2379 1842 2378 +f 2383 1842 2382 +f 2384 1842 2383 +f 2373 2361 2385 +f 2379 2380 1842 +f 1842 2385 2361 +f 2384 2385 1842 +f 2258 2298 2438 +f 2377 2378 1842 +f 2375 2376 1842 +f 2386 2446 151 +f 1156 2361 849 +f 2363 849 2361 +f 930 892 1842 +f 1427 1649 1772 +f 2395 2387 2394 +f 2396 2387 2395 +f 2393 2394 2387 +f 2398 2387 2397 +f 2896 2314 2374 +f 907 2389 2387 +f 2387 2389 2390 +f 2387 2390 2391 +f 2392 2387 2391 +f 2393 2387 2392 +f 2396 2397 2387 +f 2374 2314 2312 +f 1161 907 2387 +f 2402 2404 2403 +f 2312 2401 2374 +f 2403 2404 2431 +f 2405 2402 2403 +f 2405 2403 2406 +f 1772 1649 269 +f 2407 2405 2406 +f 2409 2411 2408 +f 2406 2408 2411 +f 2406 2411 2407 +f 2408 2413 2414 +f 2408 2414 2409 +f 2417 2414 2413 +f 2442 2408 2406 +f 111 2258 2438 +f 2405 2622 2402 +f 2405 2407 2622 +f 2411 2622 2407 +f 2411 2409 2622 +f 2414 2622 2409 +f 2417 2622 2414 +f 2422 2423 2424 +f 2426 2427 2425 +f 2412 2416 2429 +f 2421 2400 2428 +f 2427 2422 2424 +f 2424 2425 2427 +f 2430 2426 2425 +f 2430 2425 2431 +f 2410 2428 2400 +f 2420 2441 2356 +f 2432 2358 2359 +f 2433 2430 2431 +f 2404 2433 2431 +f 2415 2434 2410 +f 2358 2432 2420 +f 2424 2448 2425 +f 2424 2447 2448 +f 2428 2410 2434 +f 2426 2430 2622 +f 2430 2433 2622 +f 2433 2404 2622 +f 2404 2402 2622 +f 2434 2415 2438 +f 2406 2403 2439 +f 2418 2440 2436 +f 2403 2437 2439 +f 2439 2442 2406 +f 2412 2443 2419 +f 114 2438 2415 +f 2419 2443 2435 +f 2438 2488 2434 +f 2408 2444 2413 +f 2443 2412 2429 +f 2442 2444 2408 +f 2440 2418 2441 +f 114 111 2438 +f 2441 2420 2445 +f 2446 179 151 +f 2440 2441 2445 +f 2425 2448 2450 +f 1250 2450 2448 +f 2432 2195 2420 +f 2450 2431 2425 +f 2446 2449 179 +f 2431 2450 2437 +f 2445 2420 2195 +f 2403 2431 2437 +f 2195 2432 2184 +f 2432 2359 2153 +f 2424 2423 2453 +f 2452 2453 2423 +f 2184 2432 2153 +f 2453 2447 2424 +f 2386 2388 2454 +f 2475 1382 2419 +f 2453 2361 2447 +f 2454 2446 2386 +f 2255 2436 2440 +f 2446 2456 2449 +f 2454 2456 2446 +f 2451 2483 2455 +f 1259 2444 2442 +f 2497 2449 2456 +f 2458 2459 2460 +f 2460 2459 2413 +f 2440 2445 2261 +f 2388 2400 2421 +f 2417 2413 2459 +f 2483 2451 2457 +f 2462 2465 2463 +f 2461 226 196 +f 226 225 196 +f 2255 2440 2261 +f 2464 2461 196 +f 2460 2463 2465 +f 2460 2465 2458 +f 2421 2454 2388 +f 2463 2467 2468 +f 2463 2468 2462 +f 2261 2445 2264 +f 2421 2469 2454 +f 1379 2470 2457 +f 2264 2445 2195 +f 2471 2468 2467 +f 2454 2469 2456 +f 2720 2618 540 +f 2457 2470 2466 +f 2444 2472 2413 +f 2469 2505 2456 +f 2472 2460 2413 +f 2470 2473 2466 +f 2472 2474 2460 +f 1379 1382 2470 +f 2460 2474 2463 +f 2463 2387 2467 +f 2474 2387 2463 +f 2470 2475 2473 +f 2475 2435 2473 +f 2545 2351 2546 +f 2401 2477 2476 +f 392 2517 2496 +f 2477 2401 2312 +f 2435 2475 2419 +f 2462 2468 2622 +f 2468 2471 2622 +f 2619 2622 2471 +f 2618 2619 2471 +f 2469 2421 2478 +f 2465 2462 2622 +f 2421 2428 2478 +f 2356 2477 2312 +f 2428 2434 2480 +f 2434 2488 2480 +f 2428 2480 2478 +f 2480 2486 2478 +f 2457 2466 2483 +f 2464 2399 2932 +f 2483 2466 2523 +f 540 2471 539 +f 2480 2490 2486 +f 2485 2476 2498 +f 2488 2490 2480 +f 2523 2466 2527 +f 2477 2418 2476 +f 2493 2479 2481 +f 2493 2481 2484 +f 2493 2484 2487 +f 2493 2634 303 +f 2493 2487 2489 +f 2299 2490 2488 +f 2418 2498 2476 +f 2435 2494 2473 +f 2493 2489 2491 +f 2493 2491 2492 +f 2418 2477 2441 +f 2714 2715 647 +f 184 2449 2495 +f 2493 2532 2533 +f 2441 2477 2356 +f 184 2495 187 +f 288 187 2495 +f 2495 2449 2497 +f 2485 2498 2482 +f 2497 2499 2495 +f 2496 2501 2500 +f 2501 2496 2502 +f 2495 2499 288 +f 2436 2498 2418 +f 2494 2504 2503 +f 2456 2505 2497 +f 2512 2498 2436 +f 2435 2506 2494 +f 2443 2506 2435 +f 2500 2501 2511 +f 1330 2932 2804 +f 2507 2500 2511 +f 2497 2508 2499 +f 2494 2506 2504 +f 2467 2387 539 +f 2505 2508 2497 +f 2499 2508 2524 +f 2506 2579 2504 +f 540 543 2715 +f 2715 2716 540 +f 2479 2507 2481 +f 2805 1340 2804 +f 1330 2804 1340 +f 2808 1339 2805 +f 1340 2805 1339 +f 2507 2511 2481 +f 2499 2524 350 +f 2808 2809 1339 +f 2511 2484 2481 +f 2503 2504 2013 +f 2511 2501 2513 +f 2029 2503 2013 +f 2469 2514 2505 +f 2013 2504 2104 +f 2517 523 510 +f 2484 2511 2513 +f 523 2517 392 +f 2513 2617 2487 +f 2104 2504 2579 +f 2469 2478 2514 +f 2500 392 2496 +f 2484 2513 2487 +f 2514 2526 2505 +f 2487 2617 2489 +f 2509 2510 2543 +f 2515 2455 2483 +f 361 2479 332 +f 2479 2493 332 +f 2508 2505 2526 +f 2512 2232 2510 +f 2483 2523 2515 +f 2543 2510 2232 +f 2527 2466 2473 +f 2516 2518 2517 +f 2486 2514 2478 +f 2512 2254 2232 +f 2514 2536 2526 +f 2461 2520 1447 +f 2461 1447 226 +f 2516 2519 2518 +f 2464 2521 2520 +f 2464 2520 2461 +f 2536 2514 2486 +f 2494 2527 2473 +f 2254 2512 2436 +f 2932 2521 2464 +f 2932 1330 2521 +f 2502 2517 2518 +f 2496 2517 2502 +f 2515 2542 2522 +f 2518 2519 2757 +f 2351 2545 2299 +f 2254 2436 2255 +f 2542 2515 2523 +f 2524 364 350 +f 2508 2529 2524 +f 2523 2527 2525 +f 2508 2526 2529 +f 2527 2494 2503 +f 2529 379 2524 +f 2540 390 2529 +f 2503 2525 2527 +f 2529 390 379 +f 2524 379 364 +f 2534 2537 2533 +f 2538 2528 2530 +f 2535 2528 2538 +f 2034 2522 2023 +f 2526 2540 2529 +f 2534 2539 2537 +f 2541 2530 2509 +f 2536 2540 2526 +f 2539 2528 2537 +f 2538 2530 2541 +f 2492 2531 2493 +f 2523 2525 2542 +f 2493 2531 2532 +f 2023 2522 2542 +f 2540 398 390 +f 2541 2509 2543 +f 2533 2537 2493 +f 2546 398 2540 +f 2042 2542 2525 +f 2490 2545 2486 +f 2542 2042 2023 +f 2486 2545 2536 +f 2538 2272 2535 +f 2545 2546 2536 +f 2272 2538 2541 +f 2536 2546 2540 +f 2503 2029 2525 +f 2490 2299 2545 +f 2352 2546 2351 +f 2352 2840 2546 +f 2416 2551 2429 +f 1389 1407 2416 +f 2544 2531 2492 +f 2416 2553 2551 +f 2554 2531 2544 +f 2532 2531 2554 +f 2554 2668 2558 +f 2547 2549 2550 +f 2558 2532 2554 +f 2558 2533 2532 +f 2558 2534 2533 +f 2429 2551 2570 +f 2482 2569 2548 +f 2482 2548 2485 +f 2547 2561 2553 +f 2553 2561 2551 +f 2559 2562 2567 +f 2561 2547 2550 +f 2528 2555 2530 +f 2563 2284 2252 +f 2252 2552 2563 +f 2563 2566 2284 +f 2555 2565 2530 +f 2668 2567 2558 +f 2443 2429 2581 +f 2562 2534 2567 +f 2509 2530 2565 +f 2581 2506 2443 +f 2558 2567 2534 +f 2548 2569 2653 +f 2287 2566 2568 +f 2565 2653 2569 +f 2562 2564 2539 +f 2534 2562 2539 +f 2581 2429 2570 +f 2571 2563 2552 +f 2565 2569 2509 +f 2552 2556 2571 +f 2551 2573 2570 +f 2561 2573 2551 +f 133 135 1260 +f 2563 2574 2566 +f 2482 2510 2569 +f 2571 2574 2563 +f 2574 2576 2566 +f 2509 2569 2510 +f 2572 2586 2575 +f 2589 1177 2578 +f 2566 2576 2568 +f 2510 2482 2512 +f 2586 2572 2577 +f 2482 2498 2512 +f 2576 2580 2568 +f 2556 2557 2590 +f 2506 2581 2579 +f 2559 2744 2583 +f 2575 2585 2583 +f 2597 2581 2570 +f 2556 2590 2571 +f 2590 2594 2571 +f 2585 2575 2586 +f 2571 2594 2574 +f 2559 2583 2562 +f 2570 2573 2602 +f 2583 2585 2564 +f 133 1260 1006 +f 2564 2562 2583 +f 2594 2599 2574 +f 2597 2570 2602 +f 2550 2589 2561 +f 2561 2589 2573 +f 2573 2589 2578 +f 2600 2590 2557 +f 2578 1192 2131 +f 2579 2121 2104 +f 2595 2582 2584 +f 2593 2582 2595 +f 2581 2597 2579 +f 2584 2614 2595 +f 2605 2592 2596 +f 2579 2597 2121 +f 2109 2121 2597 +f 2598 2614 2584 +f 2603 2598 2588 +f 2602 2573 2578 +f 2602 2109 2597 +f 2557 2560 2600 +f 2600 2604 2590 +f 2614 2598 2603 +f 2578 2131 2602 +f 2587 2605 2591 +f 2592 2605 2587 +f 2588 2401 2603 +f 2590 2604 2594 +f 2109 2602 2131 +f 2604 2606 2594 +f 2591 2605 2652 +f 2374 2401 2588 +f 2605 2596 2664 +f 2062 2607 2608 +f 2594 2606 2599 +f 2455 2607 2451 +f 2606 2609 2599 +f 2734 2596 2723 +f 2607 2455 2608 +f 2616 2608 2455 +f 2599 2609 2640 +f 2610 2608 2616 +f 2616 2455 2515 +f 2427 2426 2622 +f 2623 2452 2622 +f 2624 2452 2623 +f 2625 2452 2624 +f 2699 2620 2617 +f 2626 2452 2625 +f 2616 2515 2522 +f 2627 2452 2626 +f 2628 2452 2627 +f 2633 2616 2522 +f 2630 2452 2628 +f 2631 2452 2630 +f 2632 2452 2631 +f 2603 2401 2476 +f 2613 1505 1499 +f 2610 2633 2613 +f 2459 2458 2622 +f 2458 2465 2622 +f 2633 2610 2616 +f 2476 2485 2603 +f 2647 2612 2544 +f 2634 2635 2619 +f 2634 2619 2618 +f 2635 2636 2622 +f 2635 2622 2619 +f 2662 2544 2612 +f 2747 303 2634 +f 2633 1505 2613 +f 2636 2638 2623 +f 2636 2623 2622 +f 2522 2034 2633 +f 2638 2639 2624 +f 2638 2624 2623 +f 2599 2576 2574 +f 2636 2635 2493 +f 2640 2580 2576 +f 2639 2641 2625 +f 2639 2625 2624 +f 2620 2642 2617 +f 2629 2642 2620 +f 2595 2548 2653 +f 2647 2642 2629 +f 2641 2643 2626 +f 2641 2626 2625 +f 2034 1505 2633 +f 2643 2644 2627 +f 2643 2627 2626 +f 2614 2548 2595 +f 2549 1136 2550 +f 1138 2550 1136 +f 2617 2642 2489 +f 2599 2640 2576 +f 2644 2645 2628 +f 2644 2628 2627 +f 1138 1177 2550 +f 2589 2550 1177 +f 2614 2603 2485 +f 2640 2646 2580 +f 2489 2642 2491 +f 2612 2647 2629 +f 2645 2648 2630 +f 2645 2630 2628 +f 1192 2578 1177 +f 2644 2643 2493 +f 2641 2493 2643 +f 2614 2485 2548 +f 2648 2649 2631 +f 2648 2631 2630 +f 2642 2647 2491 +f 2645 2644 2493 +f 2492 2647 2544 +f 2649 2650 2632 +f 2649 2632 2631 +f 138 2632 142 +f 2650 142 2632 +f 2492 2491 2647 +f 2650 2649 2493 +f 2648 2493 2649 +f 2555 2637 2565 +f 2634 2618 2720 +f 2653 2593 2595 +f 2593 2653 2637 +f 2648 2645 2493 +f 2639 2638 2493 +f 2639 2493 2641 +f 2638 2636 2493 +f 2635 2634 2493 +f 2596 2734 2670 +f 2621 2654 2651 +f 2637 2653 2565 +f 2658 1883 2657 +f 2655 1883 2669 +f 2660 2056 2659 +f 2652 2662 2612 +f 2663 2056 2661 +f 2609 2611 2640 +f 2652 2664 2662 +f 2664 2668 2662 +f 2640 2611 2646 +f 183 2667 1883 +f 2596 2670 2664 +f 1883 2667 2669 +f 2615 2651 2646 +f 1534 194 2325 +f 2611 2615 2646 +f 2670 2734 2559 +f 28 2671 2672 +f 28 2672 29 +f 2668 2664 2670 +f 2655 2673 2675 +f 2655 2675 2656 +f 30 2674 2671 +f 30 2671 28 +f 2670 2559 2567 +f 2651 2615 2621 +f 2656 2675 2676 +f 2656 2676 2657 +f 2567 2668 2670 +f 31 2677 2674 +f 31 2674 30 +f 2657 2676 2678 +f 2657 2678 2658 +f 2662 2668 2554 +f 32 2679 2677 +f 32 2677 31 +f 2554 2544 2662 +f 2675 2673 2272 +f 2673 2697 2272 +f 2621 2686 2654 +f 2658 2678 2680 +f 2658 2680 2659 +f 33 2681 2679 +f 33 2679 32 +f 2676 2675 2272 +f 2680 2682 2660 +f 2680 2660 2659 +f 2518 2683 2502 +f 2678 2676 2272 +f 34 2684 2681 +f 34 2681 33 +f 2682 2685 2661 +f 2682 2661 2660 +f 2680 2678 2272 +f 35 2687 2684 +f 35 2684 34 +f 2685 2688 2663 +f 2685 2663 2661 +f 2681 2549 2679 +f 2687 2549 2684 +f 1738 2693 2272 +f 2682 2680 2272 +f 2591 2689 2601 +f 36 2690 2687 +f 36 2687 35 +f 2688 2691 2665 +f 2688 2665 2663 +f 2665 2691 2693 +f 2665 2693 2666 +f 37 2694 2690 +f 37 2690 36 +f 2316 2057 2056 +f 2694 2696 2549 +f 2689 2591 2652 +f 217 2695 2667 +f 217 2667 183 +f 2696 2694 37 +f 2696 37 38 +f 2695 2697 2669 +f 2695 2669 2667 +f 2692 2698 82 +f 217 2272 2695 +f 1430 2696 38 +f 1430 38 20 +f 1875 1878 2059 +f 2655 2669 2673 +f 2697 2673 2669 +f 2605 2664 2652 +f 2695 2272 2697 +f 1430 1136 2696 +f 1136 2549 2696 +f 82 2698 83 +f 2683 2699 2502 +f 2502 2699 2501 +f 2672 427 29 +f 2683 2620 2699 +f 2560 358 2600 +f 2700 2701 90 +f 2700 90 89 +f 2100 1499 2144 +f 358 2604 2600 +f 90 2701 2705 +f 90 2705 92 +f 2689 2702 2601 +f 358 2606 2604 +f 2144 1502 2142 +f 2702 2620 2683 +f 2706 647 2704 +f 358 2609 2606 +f 1520 2169 2142 +f 92 2705 2709 +f 92 2709 94 +f 2702 2689 2629 +f 358 2611 2609 +f 2701 2607 2705 +f 358 2615 2611 +f 2620 2702 2629 +f 2711 647 2710 +f 2189 2169 1536 +f 2712 647 2711 +f 94 2709 2713 +f 94 2713 95 +f 358 2621 2615 +f 2710 647 2708 +f 2689 2652 2612 +f 2709 2705 2607 +f 2612 2629 2689 +f 2621 358 2686 +f 2716 2718 540 +f 95 2713 2717 +f 95 2717 96 +f 2699 2513 2501 +f 2272 2270 1738 +f 358 2692 2686 +f 2713 2709 2607 +f 2719 540 2718 +f 2513 2699 2617 +f 358 2698 2692 +f 2720 540 2719 +f 96 2717 2721 +f 96 2721 97 +f 358 1530 2698 +f 2717 2713 2607 +f 2691 2272 2693 +f 2685 2272 2688 +f 2721 2722 144 +f 2721 144 97 +f 2703 647 2771 +f 2704 647 2703 +f 2707 647 2706 +f 2708 647 2707 +f 2714 647 2712 +f 144 2722 2724 +f 144 2724 146 +f 146 2724 2096 +f 2727 2728 2704 +f 2727 2704 2703 +f 2721 2607 2722 +f 2724 2607 2096 +f 2722 2607 2724 +f 2728 2730 2706 +f 2728 2706 2704 +f 2802 303 2727 +f 2062 2097 2607 +f 2730 2731 2707 +f 2730 2707 2706 +f 2700 89 152 +f 2287 2568 2345 +f 2596 2592 2723 +f 2731 2732 2708 +f 2731 2708 2707 +f 1375 2451 2607 +f 1432 1375 2607 +f 2734 2723 2726 +f 2035 2777 2770 +f 2732 2735 2710 +f 2732 2710 2708 +f 2451 1375 2457 +f 2777 2608 2775 +f 2735 2737 2711 +f 2735 2711 2710 +f 2732 2731 303 +f 2730 303 2731 +f 2733 2345 2568 +f 2475 2470 1382 +f 2100 2783 1499 +f 2737 2738 2712 +f 2737 2712 2711 +f 2733 2739 2725 +f 2736 2572 2575 +f 1382 1383 2419 +f 2738 2740 2714 +f 2738 2714 2712 +f 2735 2732 303 +f 1383 1385 2419 +f 2740 2741 2715 +f 2740 2715 2714 +f 2575 2744 2736 +f 2725 2739 2729 +f 2737 2735 303 +f 1389 2416 2412 +f 2741 2742 2716 +f 2741 2716 2715 +f 2738 2737 303 +f 2726 2744 2734 +f 2744 2726 2736 +f 2742 2745 2718 +f 2742 2718 2716 +f 2740 2738 303 +f 2553 1407 2547 +f 2745 2746 2719 +f 2745 2719 2718 +f 2559 2734 2744 +f 2739 2743 2729 +f 1410 2549 2547 +f 2741 2740 303 +f 2583 2744 2575 +f 2746 2747 2720 +f 2746 2720 2719 +f 2745 2742 303 +f 2634 2720 2747 +f 1432 2607 2700 +f 2721 2717 2607 +f 2700 2607 2701 +f 2747 2746 303 +f 2746 2745 303 +f 2672 2671 2549 +f 2730 2728 303 +f 2568 2580 2733 +f 2742 2741 303 +f 2671 2674 2549 +f 2674 2677 2549 +f 2677 2679 2549 +f 2549 2681 2684 +f 2687 2690 2549 +f 2690 2694 2549 +f 196 264 553 +f 621 554 331 +f 2756 1784 2755 +f 2758 1784 2756 +f 2760 1784 2758 +f 2886 2759 2519 +f 2761 1784 2760 +f 2764 1784 2763 +f 2519 2759 2757 +f 794 797 2792 +f 2759 2748 2601 +f 2763 1784 2762 +f 641 637 341 +f 2762 1784 2761 +f 2765 1784 2764 +f 2757 2759 2601 +f 63 1 341 +f 2748 2587 2591 +f 2766 2767 1784 +f 2769 647 2767 +f 2601 2748 2591 +f 4 48 2819 +f 4 2819 1409 +f 2683 2518 2757 +f 1784 2767 647 +f 2757 2702 2683 +f 647 2769 2771 +f 2702 2757 2601 +f 160 163 1136 +f 2313 2315 2753 +f 2310 2313 2753 +f 2308 2310 2753 +f 2754 1784 2753 +f 2309 2308 2753 +f 2755 1784 2754 +f 2315 2318 2753 +f 2768 39 22 +f 1153 1139 165 +f 2452 1879 2453 +f 2765 2766 1784 +f 1781 2753 1784 +f 1879 2361 2453 +f 2423 2422 2622 +f 167 1148 165 +f 2773 2774 2750 +f 2773 2750 2749 +f 2733 2772 2739 +f 170 1159 167 +f 2774 2776 2751 +f 2774 2751 2750 +f 2773 1738 2774 +f 1246 1159 170 +f 2733 2580 2772 +f 2776 2778 2752 +f 2776 2752 2751 +f 2772 2779 2739 +f 2778 2780 2753 +f 2778 2753 2752 +f 2780 2781 2754 +f 2780 2754 2753 +f 2272 207 2493 +f 2780 2778 1738 +f 2781 2782 2755 +f 2781 2755 2754 +f 2739 2779 2743 +f 2459 2622 2417 +f 2427 2622 2422 +f 540 2618 2471 +f 2782 2784 2756 +f 2782 2756 2755 +f 2785 2743 2779 +f 2781 2780 1738 +f 2610 2613 2775 +f 2783 2775 2613 +f 2784 2786 2758 +f 2784 2758 2756 +f 510 2516 2517 +f 2784 2782 1738 +f 2782 2781 1738 +f 2786 2787 2760 +f 2786 2760 2758 +f 2819 160 1409 +f 2779 2654 2785 +f 2786 2784 1738 +f 2500 2507 361 +f 84 1932 27 +f 2768 2743 2785 +f 1480 1935 1932 +f 361 2507 2479 +f 1951 1480 1482 +f 2786 1738 2787 +f 2613 1499 2783 +f 303 332 2493 +f 1951 1482 1953 +f 1483 1953 1482 +f 2761 2760 2789 +f 2787 2789 2760 +f 1483 1564 1953 +f 2768 2788 39 +f 2789 2791 2762 +f 2789 2762 2761 +f 1249 1250 2447 +f 2787 1738 2789 +f 2353 2350 1569 +f 2785 2788 2768 +f 2791 2793 2763 +f 2791 2763 2762 +f 2788 2795 39 +f 2793 2796 2764 +f 2793 2764 2763 +f 2321 2353 1533 +f 1569 1533 2353 +f 2791 2789 1738 +f 1534 2325 1533 +f 2321 1533 2325 +f 2796 2797 2765 +f 2796 2765 2764 +f 87 2325 194 +f 2797 2798 2766 +f 2797 2766 2765 +f 1352 1339 2809 +f 68 29 427 +f 2793 1738 2796 +f 1352 2809 510 +f 2516 510 2809 +f 2766 2798 2799 +f 2766 2799 2767 +f 2807 2790 2792 +f 152 433 2700 +f 39 2795 41 +f 2803 2790 2807 +f 2799 2800 2769 +f 2799 2769 2767 +f 80 41 2795 +f 2801 2792 2794 +f 1375 1377 2457 +f 2800 2802 2771 +f 2800 2771 2769 +f 2799 303 2800 +f 2798 1738 2799 +f 2795 83 80 +f 2457 1377 1379 +f 2703 2771 2727 +f 2802 2727 2771 +f 2794 2777 2801 +f 2802 2800 303 +f 41 80 73 +f 2693 2773 2749 +f 2693 2749 2666 +f 2777 2794 2770 +f 2772 2580 2646 +f 2778 2776 1738 +f 2776 2774 1738 +f 2693 1738 2773 +f 2412 2419 1385 +f 2728 2727 303 +f 2797 2796 1738 +f 2412 1385 1389 +f 2749 2318 2666 +f 2553 2416 1407 +f 2798 2797 1738 +f 2779 2772 2651 +f 1410 2547 1407 +f 2646 2651 2772 +f 303 2799 1738 +f 2549 1410 1406 +f 2651 2654 2779 +f 433 1432 2700 +f 2549 1406 2672 +f 427 2672 1406 +f 2806 2785 2654 +f 2805 2804 2864 +f 2654 2686 2806 +f 2807 2792 2801 +f 2806 2788 2785 +f 2808 2886 2809 +f 2803 2807 2854 +f 2887 2808 2805 +f 2788 82 2795 +f 2808 2887 2886 +f 2806 82 2788 +f 2809 2519 2516 +f 625 335 628 +f 2807 2801 2877 +f 336 337 628 +f 2795 82 83 +f 2854 2807 2877 +f 637 339 341 +f 2813 2810 2811 +f 2812 2810 2813 +f 2806 2692 82 +f 2686 2692 2806 +f 2775 2801 2777 +f 2839 2812 2813 +f 46 2814 2815 +f 46 2815 47 +f 47 2815 2819 +f 47 2819 48 +f 2814 160 2815 +f 2332 2339 2817 +f 237 2822 259 +f 2823 2818 2820 +f 2830 2818 2823 +f 2820 2825 2823 +f 239 2826 2822 +f 239 2822 237 +f 2826 260 2822 +f 2825 2820 2827 +f 2827 2810 2812 +f 241 2829 2826 +f 241 2826 239 +f 2850 2364 2821 +f 2812 2825 2827 +f 1434 2829 241 +f 2816 2841 2828 +f 241 201 1434 +f 2339 2729 2817 +f 2841 2816 2831 +f 2729 2832 2817 +f 2817 2832 2821 +f 2843 2823 2825 +f 766 786 2831 +f 2832 22 2821 +f 175 1246 170 +f 175 188 1246 +f 2831 2834 2833 +f 2832 2768 22 +f 1262 188 1263 +f 2836 2834 2824 +f 2858 2821 22 +f 2833 2834 2836 +f 22 25 2858 +f 1265 240 1266 +f 2835 2838 2837 +f 2803 2824 2790 +f 2812 2839 2835 +f 2743 2832 2729 +f 2838 2835 2839 +f 2824 2803 2836 +f 2743 2768 2832 +f 1330 260 1434 +f 2829 1434 260 +f 2577 2837 2838 +f 1136 1409 160 +f 2819 2815 160 +f 2577 2572 2837 +f 398 2546 2840 +f 553 264 295 +f 85 398 2840 +f 2355 2842 2352 +f 295 296 553 +f 2831 2833 2841 +f 554 553 296 +f 2352 2842 2840 +f 2842 88 2840 +f 2835 2843 2825 +f 2841 2833 2893 +f 2835 2825 2812 +f 554 296 331 +f 2840 88 85 +f 624 331 333 +f 2842 2355 2847 +f 2836 2866 2833 +f 2849 2846 2830 +f 333 335 624 +f 625 624 335 +f 2893 2833 2866 +f 2842 93 88 +f 2846 2848 2845 +f 2849 2830 2823 +f 336 628 335 +f 2847 93 2842 +f 2849 2823 2843 +f 2846 2849 2848 +f 636 628 337 +f 339 637 636 +f 2843 2856 2849 +f 2360 2850 2847 +f 337 339 636 +f 2848 2849 2856 +f 2837 2843 2835 +f 2364 2850 2360 +f 2856 2843 2837 +f 2850 2851 2847 +f 2836 2852 2866 +f 2723 2844 2845 +f 2850 2858 2851 +f 2852 2836 2803 +f 281 2853 52 +f 281 52 51 +f 2592 2844 2723 +f 2726 2845 2848 +f 2852 2803 2854 +f 2851 93 2847 +f 2723 2845 2726 +f 52 2853 2855 +f 52 2855 53 +f 2726 2848 2736 +f 281 160 2853 +f 53 2855 2857 +f 53 2857 54 +f 2856 2736 2848 +f 54 2857 2859 +f 54 2859 55 +f 2736 2856 2572 +f 2855 2853 160 +f 2858 2850 2821 +f 2572 2856 2837 +f 55 2859 2860 +f 55 2860 56 +f 2857 2855 160 +f 2858 9 2851 +f 56 2860 2862 +f 56 2862 57 +f 2877 2801 2775 +f 2859 2857 160 +f 2858 25 9 +f 2804 2861 2864 +f 57 2862 2865 +f 57 2865 58 +f 58 2865 2814 +f 58 2814 46 +f 2814 2865 160 +f 2860 160 2862 +f 2865 2862 160 +f 254 2870 2281 +f 2867 2869 2868 +f 2871 260 2870 +f 358 244 1419 +f 2866 2852 2882 +f 255 2871 2870 +f 255 2870 254 +f 256 2872 2871 +f 256 2871 255 +f 2863 2866 2882 +f 2861 2873 2864 +f 2867 2873 2861 +f 2874 2852 2854 +f 257 2875 2872 +f 257 2872 256 +f 2867 2868 2873 +f 2882 2852 2874 +f 258 2876 2875 +f 258 2875 257 +f 259 2878 2876 +f 259 2876 258 +f 2872 260 2871 +f 2878 260 2876 +f 2889 2868 2891 +f 259 2822 2878 +f 2854 2877 2885 +f 2875 260 2872 +f 2822 260 2878 +f 2869 2879 2868 +f 2877 2775 2783 +f 1139 1136 163 +f 1139 163 165 +f 2879 2869 2880 +f 2880 2883 2879 +f 2783 2885 2877 +f 2880 2881 2883 +f 2830 2881 2818 +f 2881 2830 2883 +f 2323 2863 2292 +f 2868 2879 2891 +f 1262 1246 188 +f 2879 2897 2891 +f 190 1263 188 +f 2883 2897 2879 +f 1263 190 1265 +f 240 1265 190 +f 2292 2863 2882 +f 2897 2883 2846 +f 243 1266 240 +f 2883 2830 2846 +f 2887 2805 2864 +f 260 1330 243 +f 1266 243 1330 +f 2882 2874 2338 +f 2884 2887 2864 +f 2826 2829 260 +f 2873 2884 2864 +f 2873 2868 2889 +f 2875 2876 260 +f 2873 2889 2884 +f 1530 358 1419 +f 2338 2874 2098 +f 2854 2885 2874 +f 448 281 51 +f 2098 2874 2885 +f 2887 2884 2888 +f 2100 2885 2783 +f 2886 2887 2888 +f 2884 2895 2888 +f 2884 2889 2895 +f 2885 2100 2098 +f 1133 2898 3021 +f 3022 1163 3021 +f 1133 3021 1163 +f 2889 2891 2890 +f 3022 1202 1163 +f 2892 2828 2841 +f 2890 2891 2844 +f 1251 1260 1554 +f 1574 1649 1376 +f 2886 2519 2809 +f 2886 2888 2759 +f 1211 1044 1216 +f 2841 2893 2892 +f 2888 2748 2759 +f 1044 1220 1216 +f 1221 1220 1081 +f 1081 1228 1221 +f 1157 1229 1228 +f 2889 2890 2895 +f 1229 1157 1234 +f 2888 2895 2748 +f 1427 1376 1649 +f 1376 1356 1574 +f 1585 1574 1356 +f 2890 2587 2895 +f 1306 1597 1356 +f 1585 1356 1597 +f 1597 1306 1527 +f 1267 1528 1527 +f 1275 1532 1528 +f 2748 2895 2587 +f 2901 2892 2893 +f 1548 1244 1554 +f 2890 2844 2592 +f 135 1554 1260 +f 2896 2892 2901 +f 2587 2890 2592 +f 2863 2901 2893 +f 2894 2899 2898 +f 2844 2891 2897 +f 2863 2893 2866 +f 2959 2900 2894 +f 2844 2897 2845 +f 2845 2897 2846 +f 2894 2900 2899 +f 2929 2928 2804 +f 2896 2901 2314 +f 2452 1883 1879 +f 2361 1879 1842 +f 2323 2901 2863 +f 2452 2423 2622 +f 2903 2813 2902 +f 2314 2901 2323 +f 2582 2577 2838 +f 2577 2582 2586 +f 2493 2537 2272 +f 2902 2811 2956 +f 2361 1156 2447 +f 2902 2920 2903 +f 2920 2902 2904 +f 1259 2472 2444 +f 2362 2907 2908 +f 2362 2908 2363 +f 2365 2909 2907 +f 2365 2907 2362 +f 2366 2912 2909 +f 2366 2909 2365 +f 2367 2913 2912 +f 2367 2912 2366 +f 2904 2911 2905 +f 2368 2914 2913 +f 2368 2913 2367 +f 2914 2811 2913 +f 2369 2915 2914 +f 2369 2914 2368 +f 2828 2911 2816 +f 2370 2916 2915 +f 2370 2915 2369 +f 2912 2913 2811 +f 2371 2917 2916 +f 2371 2916 2370 +f 2917 2811 2916 +f 2911 2828 2905 +f 2372 2918 2917 +f 2372 2917 2371 +f 2915 2811 2914 +f 2918 2811 2917 +f 2373 2919 2918 +f 2373 2918 2372 +f 2915 2916 2811 +f 2919 2956 2811 +f 2921 2922 2390 +f 2921 2390 2389 +f 2922 2923 2391 +f 2922 2391 2390 +f 2921 2804 2922 +f 2923 2924 2392 +f 2923 2392 2391 +f 2904 2905 2920 +f 2924 2925 2393 +f 2924 2393 2392 +f 2934 2903 2920 +f 2925 2926 2394 +f 2925 2394 2393 +f 2925 2924 2804 +f 2923 2804 2924 +f 2926 2927 2395 +f 2926 2395 2394 +f 2926 2925 2804 +f 2927 2928 2396 +f 2927 2396 2395 +f 2927 2926 2804 +f 2928 2929 2397 +f 2928 2397 2396 +f 2929 2930 2398 +f 2929 2398 2397 +f 2398 2930 2932 +f 2398 2932 2399 +f 2932 2930 2804 +f 2929 2804 2930 +f 941 2921 2389 +f 941 2389 907 +f 2227 2869 2867 +f 2979 2996 2931 +f 2869 2227 2880 +f 2229 2880 2227 +f 2881 2880 2229 +f 2934 2933 2903 +f 2928 2927 2804 +f 2804 2923 2922 +f 941 2174 2921 +f 2811 2908 2907 +f 2909 2811 2907 +f 2912 2811 2909 +f 2918 2919 2811 +f 2936 911 2935 +f 858 2935 911 +f 2935 2937 2936 +f 2593 2637 2586 +f 2528 2539 2564 +f 2935 2938 2937 +f 2528 2535 2537 +f 2537 2535 2272 +f 2933 2934 2957 +f 2938 2940 2937 +f 2934 2941 2957 +f 2056 2059 2659 +f 2059 1878 2659 +f 2942 2920 2905 +f 2838 2939 2582 +f 2920 2942 2934 +f 2586 2582 2593 +f 3049 2940 2938 +f 2585 2586 2637 +f 2555 2564 2637 +f 2564 2585 2637 +f 2941 2934 2942 +f 2555 2528 2564 +f 2942 2943 2941 +f 2682 2272 2685 +f 2944 2942 2905 +f 2903 2933 2813 +f 2944 2905 2828 +f 2939 2838 2933 +f 2892 2944 2828 +f 2946 2947 2376 +f 2946 2376 2375 +f 2376 2947 2948 +f 2376 2948 2377 +f 2942 2944 2943 +f 2947 2902 2948 +f 2946 2902 2947 +f 2377 2948 2949 +f 2377 2949 2378 +f 2378 2949 2950 +f 2378 2950 2379 +f 2948 2902 2949 +f 2379 2950 2951 +f 2379 2951 2380 +f 2950 2949 2902 +f 2944 2892 2896 +f 2380 2951 2952 +f 2380 2952 2381 +f 2951 2950 2902 +f 2381 2952 2953 +f 2381 2953 2382 +f 2952 2951 2902 +f 2382 2953 2954 +f 2382 2954 2383 +f 2953 2952 2902 +f 2383 2954 2955 +f 2383 2955 2384 +f 2896 2943 2944 +f 2384 2955 2956 +f 2384 2956 2385 +f 2955 2954 2902 +f 2954 2953 2902 +f 2956 2373 2385 +f 2956 2919 2373 +f 2956 2955 2902 +f 2939 2584 2582 +f 1249 2447 1156 +f 2448 2447 1250 +f 2933 2957 2939 +f 1257 2442 2439 +f 2906 911 2936 +f 2584 2939 2957 +f 2936 2937 2910 +f 2906 2936 2910 +f 2598 2957 2941 +f 2867 2224 2227 +f 2937 2940 2931 +f 2584 2957 2598 +f 2231 2881 2229 +f 2910 2937 2931 +f 2231 2204 2881 +f 2818 2204 2820 +f 2979 2931 2940 +f 3049 2958 2940 +f 2588 2941 2943 +f 2598 2941 2588 +f 2324 2946 892 +f 2375 892 2946 +f 2943 2896 2374 +f 1250 1253 2450 +f 2940 2958 2979 +f 1253 1255 2450 +f 1257 2439 2437 +f 1257 1258 2442 +f 2588 2943 2374 +f 1258 1259 2442 +f 2811 2902 2813 +f 2474 2472 1547 +f 1545 1547 2472 +f 2474 1547 2387 +f 1161 2387 1547 +f 2839 2813 2933 +f 849 2363 935 +f 2908 935 2363 +f 2839 2933 2838 +f 2223 2861 2174 +f 2804 2174 2861 +f 2224 2867 2223 +f 2861 2223 2867 +f 2818 2881 2204 +f 2206 2820 2204 +f 2820 2206 2208 +f 2820 2208 2827 +f 2945 2980 2987 +f 2827 2208 2810 +f 2213 2810 2208 +f 2810 2213 2216 +f 2810 2216 2811 +f 2804 2921 2174 +f 2811 2216 2908 +f 935 2908 2216 +f 2666 2056 2665 +f 2688 2272 2691 +f 2931 2960 2910 +f 1161 2253 908 +f 612 2959 2894 +f 1059 908 2253 +f 2910 2961 2906 +f 2906 2961 2959 +f 2961 2910 2960 +f 128 1333 1344 +f 2900 2959 2961 +f 1351 136 1344 +f 1655 2962 2963 +f 1655 2963 1656 +f 2961 2960 2968 +f 1233 3060 1182 +f 1657 2964 2962 +f 1657 2962 1655 +f 2900 2961 2968 +f 1305 1321 110 +f 1321 1326 110 +f 1658 2965 2964 +f 1658 2964 1657 +f 128 1344 136 +f 1351 2174 136 +f 1659 2966 2965 +f 1659 2965 1658 +f 1661 2967 2966 +f 1661 2966 1659 +f 11 1181 1311 +f 2967 2770 2966 +f 1663 2969 2967 +f 1663 2967 1661 +f 1665 2970 2969 +f 1665 2969 1663 +f 2965 2966 2770 +f 1666 2971 2970 +f 1666 2970 1665 +f 2969 2770 2967 +f 1668 2972 2971 +f 1668 2971 1666 +f 2972 2973 2770 +f 1669 2973 2972 +f 1669 2972 1668 +f 2035 2770 2973 +f 2900 2968 2974 +f 2899 2900 2974 +f 2971 2972 2770 +f 2970 2971 2770 +f 2977 2968 2975 +f 766 2831 2816 +f 2387 2399 196 +f 2464 196 2399 +f 2387 2398 2399 +f 2977 2975 2976 +f 2467 539 2471 +f 196 539 2387 +f 2902 2946 806 +f 2968 2977 2974 +f 2770 2963 2962 +f 2770 2962 2964 +f 2965 2770 2964 +f 2770 2969 2970 +f 895 2088 1650 +f 2974 2977 3026 +f 946 1970 1967 +f 930 1842 1978 +f 2976 3013 3031 +f 1676 1656 2963 +f 3030 2976 3031 +f 2834 786 2824 +f 789 2790 2824 +f 2986 2958 2945 +f 2958 2986 2979 +f 1094 2008 2091 +f 946 1967 2012 +f 1673 2981 1756 +f 1674 2982 2981 +f 1674 2981 1673 +f 2980 2978 2983 +f 2982 2984 734 +f 2984 2982 1674 +f 2984 1674 1676 +f 2963 2984 1676 +f 2987 2980 2983 +f 2978 2985 2983 +f 810 2911 807 +f 810 762 2911 +f 2985 2978 3058 +f 762 766 2816 +f 789 2824 786 +f 794 2792 2790 +f 2794 2792 797 +f 734 2770 797 +f 2324 806 2946 +f 2984 2963 734 +f 2770 734 2963 +f 1092 2088 895 +f 1094 2091 2088 +f 1094 2088 1092 +f 2945 2987 2986 +f 1066 2008 1094 +f 2010 2008 1069 +f 1069 2008 1066 +f 1072 2012 1069 +f 2010 1069 2012 +f 2986 2987 2995 +f 2012 1072 946 +f 949 1970 946 +f 949 950 1970 +f 1973 1970 950 +f 951 1978 950 +f 1973 950 1978 +f 2983 2985 3000 +f 951 930 1978 +f 1679 2988 836 +f 1679 836 1680 +f 1682 2989 2988 +f 1682 2988 1679 +f 836 2988 734 +f 2989 734 2988 +f 2989 1682 2990 +f 1683 2990 1682 +f 2990 734 2989 +f 1685 2991 2990 +f 1685 2990 1683 +f 2991 734 2990 +f 1744 2992 2991 +f 1744 2991 1685 +f 1755 2993 2992 +f 1755 2992 1744 +f 2979 2998 2996 +f 1756 2994 2993 +f 1756 2993 1755 +f 2992 734 2991 +f 2979 2986 2998 +f 1756 2981 2994 +f 2986 2995 2998 +f 2981 734 2994 +f 734 2992 2993 +f 2902 806 2904 +f 807 2904 806 +f 2816 2911 762 +f 2834 2831 786 +f 2960 2975 2968 +f 791 2790 789 +f 791 794 2790 +f 2960 2931 2996 +f 2794 797 2770 +f 2996 2975 2960 +f 2975 2996 2997 +f 2993 2994 734 +f 2981 2982 734 +f 1277 107 1311 +f 1277 1238 107 +f 2996 2998 2997 +f 2995 3003 2998 +f 1212 3001 3002 +f 1223 3010 1212 +f 3001 1212 3010 +f 3003 2997 2998 +f 118 119 1354 +f 118 1354 1357 +f 118 1357 125 +f 1357 734 125 +f 3005 2987 2983 +f 3004 327 2999 +f 3004 172 327 +f 2999 3001 3004 +f 2999 3016 3002 +f 3001 2999 3002 +f 2995 3011 3003 +f 2995 2987 3005 +f 3005 2983 3000 +f 3004 3009 172 +f 3005 3011 2995 +f 3005 3000 3006 +f 3005 3006 3011 +f 171 3007 238 +f 2985 3008 3000 +f 3009 171 172 +f 3061 3008 2985 +f 3004 3010 3009 +f 3007 171 3009 +f 3018 3008 104 +f 3010 3004 3001 +f 2 104 3008 +f 3009 3010 3012 +f 3003 3013 2997 +f 1230 3012 3010 +f 548 238 3007 +f 3007 119 548 +f 3009 3012 3007 +f 3003 3011 3015 +f 119 3007 3012 +f 1230 119 3012 +f 1238 3017 107 +f 2997 2976 2975 +f 304 50 60 +f 2976 2997 3013 +f 3015 3013 3003 +f 3014 62 3016 +f 3016 62 99 +f 3016 99 3017 +f 2999 3014 3016 +f 327 3014 2999 +f 3016 3017 3002 +f 3000 3018 3006 +f 3008 3018 3000 +f 1238 3002 3017 +f 3006 3019 3011 +f 3011 3019 3015 +f 359 11 363 +f 3020 3006 3018 +f 43 367 11 +f 103 3020 3018 +f 43 11 45 +f 103 3018 104 +f 45 11 50 +f 60 50 11 +f 11 62 60 +f 62 11 99 +f 3019 3006 3020 +f 3020 103 122 +f 2898 2899 3024 +f 2898 3024 3021 +f 3021 3023 3022 +f 2899 3025 3024 +f 3021 3024 3023 +f 3027 3023 3024 +f 2899 2974 3025 +f 3026 3025 2974 +f 2977 2976 3030 +f 2977 3030 3026 +f 3031 3013 3037 +f 3027 3024 3025 +f 3025 3026 3028 +f 3027 3025 3028 +f 3026 3029 3028 +f 3026 3030 3029 +f 3027 3028 3036 +f 3028 3029 3035 +f 3022 3032 1202 +f 3023 3033 3022 +f 3022 3033 3032 +f 3023 3034 3033 +f 3027 3034 3023 +f 3036 3034 3027 +f 3035 3036 3028 +f 3044 3035 3029 +f 1202 3032 160 +f 3032 3033 163 +f 160 3032 163 +f 3033 3034 165 +f 163 3033 165 +f 3034 167 165 +f 3036 167 3034 +f 3036 3035 170 +f 167 3036 170 +f 170 3035 175 +f 3015 3037 3013 +f 3019 3040 3015 +f 3037 3015 3040 +f 3029 3030 3038 +f 3031 3038 3030 +f 3037 3040 3039 +f 3040 3041 3039 +f 3042 3040 3019 +f 3020 3042 3019 +f 3042 3020 122 +f 3040 3042 3041 +f 3042 122 127 +f 127 3041 3042 +f 3037 3043 3031 +f 3031 3043 3038 +f 3043 3037 3039 +f 3041 127 3048 +f 3038 3044 3029 +f 3043 3045 3038 +f 136 3048 127 +f 175 3035 3044 +f 3044 188 175 +f 3038 3045 3044 +f 3045 188 3044 +f 3045 190 188 +f 3039 3046 3043 +f 3046 3045 3043 +f 190 3045 3046 +f 3046 240 190 +f 3041 3047 3039 +f 3046 3039 3047 +f 3046 3047 240 +f 3047 243 240 +f 3048 3047 3041 +f 243 3047 3048 +f 260 3048 136 +f 3048 260 243 +f 270 2935 1311 +f 858 1311 2935 +f 2935 270 2938 +f 3049 2938 270 +f 3049 270 3050 +f 3051 270 3054 +f 3052 270 3056 +f 2958 3049 3050 +f 2945 2958 3050 +f 3051 3053 270 +f 3054 270 3055 +f 270 3052 3055 +f 3056 270 3057 +f 270 1182 3057 +f 270 3053 3050 +f 3050 3053 2945 +f 3051 2980 3053 +f 2945 3053 2980 +f 3051 2978 2980 +f 2978 3051 3054 +f 270 1181 1182 +f 3057 1182 3060 +f 3054 3058 2978 +f 3055 3058 3054 +f 3062 3055 3052 +f 3058 3055 3062 +f 3062 3052 3059 +f 3056 3059 3052 +f 3059 3056 3060 +f 3057 3060 3056 +f 3058 3061 2985 +f 3058 3062 3061 +f 3062 3059 3063 +f 3061 3062 3063 +f 3059 3060 3064 +f 3063 3059 3064 +f 1289 3064 3060 +f 3061 3063 2 +f 23 3063 3064 +f 1289 23 3064 +f 3061 2 3008 +f 23 2 3063 +# 5336 faces + +g group_0_16089887 + +usemtl color_16089887 +s 0 + +f 417 709 435 +f 518 435 373 +f 471 476 559 +f 464 466 566 +f 569 464 566 +f 512 514 515 +f 512 515 511 +f 514 517 518 +f 514 518 515 +f 517 519 520 +f 517 520 518 +f 381 1095 525 +f 1096 1097 525 +f 519 528 529 +f 519 529 520 +f 528 530 531 +f 528 531 529 +f 530 532 533 +f 530 533 531 +f 532 534 535 +f 532 535 533 +f 535 534 556 +f 532 764 534 +f 530 759 532 +f 519 746 528 +f 435 518 520 +f 520 417 435 +f 520 529 417 +f 417 529 414 +f 531 414 529 +f 413 531 533 +f 413 414 531 +f 409 533 535 +f 544 1045 550 +f 413 533 409 +f 482 409 535 +f 1046 550 1045 +f 514 696 517 +f 748 528 746 +f 556 557 535 +f 556 558 559 +f 556 559 557 +f 558 560 561 +f 558 561 559 +f 1046 1049 550 +f 550 1049 564 +f 566 561 565 +f 560 565 561 +f 565 568 569 +f 565 569 566 +f 482 535 557 +f 476 482 557 +f 476 557 559 +f 561 471 559 +f 566 466 561 +f 466 471 561 +f 558 556 879 +f 764 880 556 +f 556 534 764 +f 576 569 575 +f 568 575 569 +f 575 580 581 +f 575 581 576 +f 544 550 1603 +f 1050 1172 564 +f 1630 564 591 +f 596 1185 597 +f 595 596 1623 +f 599 1758 616 +f 616 1644 599 +f 1802 1804 633 +f 639 638 676 +f 827 629 630 +f 515 518 373 +f 373 706 664 +f 664 665 373 +f 373 665 374 +f 374 667 378 +f 666 667 374 +f 657 648 378 +f 381 378 648 +f 381 648 649 +f 381 525 639 +f 373 374 515 +f 666 374 665 +f 511 374 378 +f 657 378 667 +f 511 677 512 +f 676 512 677 +f 512 694 514 +f 676 690 512 +f 676 677 639 +f 677 381 639 +f 378 381 677 +f 677 511 378 +f 515 374 511 +f 841 650 674 +f 1595 1129 638 +f 690 676 682 +f 638 682 676 +f 839 674 675 +f 686 682 687 +f 821 635 650 +f 690 694 512 +f 690 682 691 +f 686 691 682 +f 696 514 694 +f 694 690 695 +f 691 695 690 +f 694 697 696 +f 695 697 694 +f 700 743 701 +f 697 701 696 +f 764 727 409 +f 727 722 409 +f 413 724 414 +f 723 724 413 +f 725 715 414 +f 414 715 417 +f 373 435 706 +f 706 1381 664 +f 723 413 722 +f 435 709 710 +f 435 710 706 +f 725 414 724 +f 417 715 717 +f 417 717 709 +f 722 1381 723 +f 482 764 409 +f 413 409 722 +f 769 774 727 +f 728 727 774 +f 519 517 743 +f 746 519 743 +f 743 517 696 +f 696 701 743 +f 744 743 700 +f 746 743 747 +f 744 747 743 +f 760 530 748 +f 759 530 760 +f 749 748 750 +f 748 746 750 +f 752 750 746 +f 747 752 746 +f 764 532 754 +f 754 759 758 +f 757 758 759 +f 754 532 759 +f 759 760 757 +f 887 732 735 +f 528 748 530 +f 758 757 761 +f 757 760 761 +f 749 761 760 +f 748 749 760 +f 764 754 769 +f 768 769 754 +f 880 879 556 +f 758 768 754 +f 769 768 772 +f 768 758 772 +f 761 772 758 +f 769 727 764 +f 868 846 765 +f 772 774 769 +f 887 873 732 +f 476 879 482 +f 482 880 764 +f 771 729 874 +f 940 936 501 +f 926 464 936 +f 462 936 464 +f 466 464 926 +f 476 471 885 +f 462 464 569 +f 462 501 936 +f 885 471 466 +f 462 569 576 +f 940 501 499 +f 466 913 885 +f 476 885 879 +f 947 801 814 +f 820 822 823 +f 820 823 821 +f 822 824 825 +f 822 825 823 +f 824 826 827 +f 824 827 825 +f 823 635 821 +f 822 820 1918 +f 820 835 1918 +f 678 1933 831 +f 835 820 821 +f 499 945 940 +f 1933 1930 833 +f 834 833 1930 +f 831 1933 832 +f 832 1933 833 +f 839 834 841 +f 835 841 834 +f 501 462 576 +f 821 841 835 +f 841 674 839 +f 581 499 501 +f 650 841 821 +f 672 678 831 +f 859 944 814 +f 846 832 844 +f 833 844 832 +f 707 2054 499 +f 833 834 844 +f 839 844 834 +f 868 832 846 +f 844 678 846 +f 672 846 678 +f 675 678 844 +f 675 844 839 +f 672 765 846 +f 865 866 867 +f 862 2192 864 +f 861 2192 862 +f 2209 2218 866 +f 866 868 867 +f 866 832 868 +f 868 765 867 +f 765 767 867 +f 862 874 873 +f 862 873 861 +f 947 814 944 +f 874 862 875 +f 864 875 862 +f 864 865 867 +f 875 864 867 +f 953 829 801 +f 956 1988 829 +f 874 729 873 +f 729 732 873 +f 482 879 880 +f 875 771 874 +f 767 771 875 +f 867 767 875 +f 879 885 558 +f 560 558 885 +f 885 913 560 +f 883 886 887 +f 883 887 884 +f 886 873 887 +f 861 873 886 +f 565 913 568 +f 1765 581 580 +f 501 576 581 +f 499 581 1764 +f 707 499 1764 +f 728 777 787 +f 728 774 777 +f 903 787 777 +f 565 560 913 +f 926 568 913 +f 707 1764 1768 +f 903 812 787 +f 925 812 903 +f 466 926 913 +f 926 936 568 +f 575 568 936 +f 925 932 854 +f 925 854 812 +f 575 936 580 +f 940 580 936 +f 945 1765 940 +f 932 944 859 +f 932 859 854 +f 947 953 801 +f 953 955 829 +f 955 956 829 +f 526 527 1608 +f 982 980 983 +f 986 994 1624 +f 987 986 988 +f 985 988 986 +f 985 989 988 +f 1624 1622 986 +f 982 989 980 +f 985 980 989 +f 1627 1624 995 +f 994 995 1624 +f 987 997 986 +f 994 986 997 +f 996 995 994 +f 996 994 997 +f 995 1001 1627 +f 1001 995 1002 +f 996 1002 995 +f 1018 1598 1009 +f 1012 1009 1001 +f 1012 1001 1002 +f 1109 1118 1604 +f 1118 1123 1607 +f 1020 1018 1021 +f 1018 1009 1021 +f 1012 1021 1009 +f 1607 1604 1118 +f 1025 1018 1026 +f 1020 1026 1018 +f 1026 1028 1025 +f 1642 1032 1031 +f 1646 597 599 +f 527 526 1077 +f 1078 1067 527 +f 527 1068 544 +f 1070 1045 544 +f 1050 564 1049 +f 649 1095 381 +f 1067 1068 527 +f 1070 544 1068 +f 1096 525 1095 +f 525 1097 526 +f 1087 1077 526 +f 1078 527 1077 +f 1087 526 1097 +f 526 1596 525 +f 639 525 1596 +f 1595 1607 1123 +f 526 1608 1596 +f 1025 1028 1109 +f 1113 1109 1028 +f 1118 1109 1121 +f 1113 1121 1109 +f 682 638 1129 +f 1123 1129 1595 +f 1125 1123 1126 +f 1123 1118 1126 +f 1121 1126 1118 +f 1131 1129 1132 +f 1129 1123 1132 +f 1125 1132 1123 +f 1137 687 682 +f 682 1129 1137 +f 1131 1137 1129 +f 591 564 1172 +f 591 1193 592 +f 1194 1203 592 +f 595 1165 596 +f 1184 1185 596 +f 599 597 1168 +f 1168 1169 599 +f 1194 592 1193 +f 1184 596 1165 +f 1168 597 1167 +f 1186 1167 597 +f 592 1203 595 +f 595 1174 1165 +f 1167 1381 1168 +f 1186 597 1185 +f 1172 1198 591 +f 591 1198 1193 +f 1174 595 1203 +f 980 985 1622 +f 1207 1205 1208 +f 1622 1620 980 +f 980 1205 983 +f 1207 983 1205 +f 1045 1381 1046 +f 709 1381 710 +f 1077 1381 1078 +f 1068 1381 1070 +f 1095 1381 1096 +f 715 1381 717 +f 728 1381 727 +f 665 1381 666 +f 648 1381 649 +f 1097 1381 1087 +f 1049 1381 1050 +f 1165 1381 1184 +f 1172 1381 1198 +f 1203 1381 1174 +f 728 787 1381 +f 1381 2179 1889 +f 1889 1891 1381 +f 787 812 1381 +f 814 801 1381 +f 1988 1989 1381 +f 1989 1995 1381 +f 1381 2135 2120 +f 1998 2000 1381 +f 1381 2239 2240 +f 2240 2248 1381 +f 2122 2123 1381 +f 2263 2239 1381 +f 2181 2179 1381 +f 1381 2183 2181 +f 1381 2248 2178 +f 2123 2263 1381 +f 1381 2120 2122 +f 1381 2116 2134 +f 1995 1998 1381 +f 1381 2134 2135 +f 2000 2009 1381 +f 829 1988 1381 +f 1381 801 829 +f 812 854 1381 +f 859 814 1381 +f 854 859 1381 +f 1896 1381 1891 +f 1902 1736 1381 +f 1904 1902 1381 +f 1747 1746 1381 +f 1746 1751 1381 +f 1185 1184 1381 +f 1193 1198 1381 +f 1172 1050 1381 +f 1165 1174 1381 +f 1169 1168 1381 +f 1203 1194 1381 +f 1068 1067 1381 +f 706 710 1381 +f 709 717 1381 +f 715 725 1381 +f 648 657 1381 +f 1067 1078 1381 +f 1095 649 1381 +f 1077 1087 1381 +f 1045 1070 1381 +f 1097 1096 1381 +f 657 667 1381 +f 1167 1186 1381 +f 1193 1381 1194 +f 1049 1046 1381 +f 665 664 1381 +f 667 666 1381 +f 1758 1169 1381 +f 1748 1759 1381 +f 1774 1747 1381 +f 1381 1736 1774 +f 1910 1904 1381 +f 724 723 1381 +f 722 727 1381 +f 725 724 1381 +f 1910 1381 1896 +f 1381 2009 2116 +f 2183 1381 2172 +f 1607 1595 1596 +f 1595 638 639 +f 1595 639 1596 +f 1025 1109 1602 +f 1598 1599 1630 +f 1598 1602 1603 +f 1598 1603 1599 +f 1602 1604 1605 +f 1602 1605 1603 +f 1604 1607 1608 +f 1604 1608 1605 +f 1596 1608 1607 +f 564 1630 1599 +f 550 1599 1603 +f 550 564 1599 +f 1605 544 1603 +f 527 544 1605 +f 527 1605 1608 +f 1604 1602 1109 +f 1018 1025 1602 +f 1602 1598 1018 +f 1629 1009 1598 +f 1645 1620 1621 +f 1620 1622 1623 +f 1620 1623 1621 +f 1622 1624 1625 +f 1622 1625 1623 +f 1624 1627 1628 +f 1624 1628 1625 +f 1627 1629 1630 +f 1627 1630 1628 +f 1630 1629 1598 +f 596 597 1621 +f 1623 596 1621 +f 1625 595 1623 +f 592 595 1625 +f 1628 592 1625 +f 592 1628 591 +f 1630 591 1628 +f 1880 1205 1645 +f 1643 1880 1645 +f 1205 980 1620 +f 985 986 1622 +f 1009 1629 1001 +f 1627 1001 1629 +f 1031 1641 1642 +f 1641 1643 1644 +f 1641 1644 1642 +f 1643 1645 1646 +f 1643 1646 1644 +f 1621 1646 1645 +f 1646 599 1644 +f 1621 597 1646 +f 1641 1874 1643 +f 1751 1749 1381 +f 1185 1381 1186 +f 1748 1381 1749 +f 617 616 1748 +f 1749 1751 617 +f 617 1751 618 +f 629 1848 630 +f 1802 633 630 +f 635 633 1804 +f 633 825 630 +f 827 1820 629 +f 618 629 1820 +f 629 1855 1848 +f 1749 617 1748 +f 1854 618 1751 +f 1169 1758 599 +f 616 1758 1759 +f 616 1759 1748 +f 1758 1381 1759 +f 1855 629 618 +f 1764 581 1765 +f 1765 1767 1768 +f 1765 1768 1764 +f 1767 1769 1770 +f 1767 1770 1768 +f 1642 617 1032 +f 1854 1746 1747 +f 1768 705 707 +f 705 1768 1770 +f 580 940 1765 +f 1777 1770 1769 +f 1777 1769 1778 +f 1032 618 1820 +f 1032 617 618 +f 1767 1765 2054 +f 1851 1747 1774 +f 1851 1857 1747 +f 1746 1854 1751 +f 1856 1849 1939 +f 1642 1644 616 +f 617 1642 616 +f 630 1798 1802 +f 2072 1790 1923 +f 1798 1848 1818 +f 1788 1790 1805 +f 1806 1805 1790 +f 1818 1031 1032 +f 827 630 825 +f 1818 826 1798 +f 826 824 1798 +f 1820 827 1818 +f 826 1818 827 +f 1032 1820 1818 +f 823 825 633 +f 633 635 823 +f 1924 1806 698 +f 1805 1929 1853 +f 702 1777 698 +f 1804 1802 824 +f 1802 1798 824 +f 1806 1927 1805 +f 1927 1929 1805 +f 702 1770 1777 +f 702 705 1770 +f 1806 1924 1927 +f 1832 720 1834 +f 1736 1902 1828 +f 1834 1836 1832 +f 1835 1832 1836 +f 1641 1031 1869 +f 1832 1835 1945 +f 1798 630 1848 +f 1848 1857 1818 +f 1844 1835 1836 +f 1844 1836 1845 +f 1774 1736 1843 +f 1828 1843 1736 +f 2147 1956 1834 +f 1847 1844 1845 +f 1847 1845 1849 +f 1847 1940 1844 +f 1858 1869 1031 +f 1843 1851 1774 +f 1836 1834 1948 +f 1855 618 1854 +f 1853 1847 1849 +f 1853 1849 1856 +f 1938 1940 1853 +f 1848 1855 1857 +f 1857 1858 1818 +f 1031 1818 1858 +f 1942 1835 1844 +f 1855 1747 1857 +f 1854 1747 1855 +f 1857 1862 1858 +f 1861 1858 1862 +f 1851 1863 1857 +f 1857 1863 1862 +f 1788 1805 1856 +f 1853 1856 1805 +f 1869 1874 1641 +f 1847 1853 1940 +f 1869 1858 1870 +f 1861 1870 1858 +f 1880 1643 1874 +f 1835 1942 1945 +f 1877 1874 1869 +f 1870 1877 1869 +f 1620 1645 1205 +f 1881 1880 1882 +f 1880 1874 1882 +f 1877 1882 1874 +f 1849 1845 1939 +f 1941 1939 1845 +f 1944 1941 1845 +f 1886 1957 1956 +f 1886 1956 1887 +f 1205 1880 1208 +f 1881 1208 1880 +f 1948 1944 1836 +f 1948 1834 1956 +f 1899 1886 1898 +f 1887 1898 1886 +f 1898 1900 1901 +f 1898 1901 1899 +f 1900 1919 1901 +f 1899 804 1886 +f 1804 1918 635 +f 650 635 1918 +f 1949 1957 720 +f 811 720 1957 +f 1957 1886 811 +f 1886 804 811 +f 1919 1920 1901 +f 824 822 1804 +f 831 832 2218 +f 1924 1777 1923 +f 1778 1923 1777 +f 1918 1925 674 +f 1923 1926 1927 +f 1923 1927 1924 +f 1926 1928 1929 +f 1926 1929 1927 +f 1930 675 1925 +f 1918 1804 822 +f 1924 698 1777 +f 1925 1918 835 +f 1930 1925 834 +f 835 834 1925 +f 674 650 1918 +f 674 1925 675 +f 1923 1790 1926 +f 1790 1788 1926 +f 2218 765 831 +f 1928 1937 1938 +f 1928 1938 1929 +f 1930 1933 675 +f 678 675 1933 +f 1937 1939 1940 +f 1937 1940 1938 +f 1939 1941 1942 +f 1939 1942 1940 +f 1941 1944 1945 +f 1941 1945 1942 +f 1944 1948 1949 +f 1944 1949 1945 +f 1853 1929 1938 +f 1942 1844 1940 +f 1832 1945 1949 +f 1891 1947 1946 +f 1891 1946 1896 +f 720 1832 1949 +f 1928 1926 1788 +f 1788 1856 1928 +f 1939 1937 1856 +f 1937 1928 1856 +f 1948 1956 1957 +f 1948 1957 1949 +f 1910 1896 1955 +f 1946 1955 1896 +f 1904 1910 1958 +f 1955 1958 1910 +f 1845 1836 1944 +f 1902 1904 1961 +f 1958 1961 1904 +f 1829 1828 1902 +f 1961 1829 1902 +f 1963 1889 1964 +f 1889 1963 1891 +f 1963 1947 1891 +f 702 2063 705 +f 698 2072 702 +f 2054 707 705 +f 2072 698 1806 +f 2063 702 2072 +f 2087 2000 2085 +f 2000 2087 2009 +f 1806 1790 2072 +f 1995 2084 1998 +f 1998 2085 2000 +f 705 2052 2054 +f 1988 2067 1989 +f 1989 2075 1995 +f 2075 1989 2067 +f 2084 1995 2075 +f 2009 2089 2116 +f 1778 1769 2052 +f 499 2054 945 +f 1769 1767 2054 +f 2054 1765 945 +f 2052 1769 2054 +f 705 2063 2052 +f 2063 2072 1778 +f 2052 2063 1778 +f 1988 956 2067 +f 1923 1778 2072 +f 2085 1998 2084 +f 2087 2089 2009 +f 2090 2116 2089 +f 811 2278 720 +f 2147 1834 720 +f 2165 2123 2163 +f 2122 2120 2157 +f 2122 2163 2123 +f 2123 2165 2263 +f 2159 2158 2120 +f 2164 2122 2157 +f 2122 2164 2163 +f 2140 2116 2090 +f 2140 1999 2134 +f 2140 2134 2116 +f 1887 1956 2147 +f 2154 2135 1999 +f 2134 1999 2135 +f 2157 2120 2158 +f 2135 2154 2120 +f 2159 2120 2154 +f 732 2193 735 +f 732 729 2193 +f 1381 2176 2172 +f 729 2192 2193 +f 2217 2172 2176 +f 2176 1381 2178 +f 2179 1964 1889 +f 2181 2226 2179 +f 2228 1964 2179 +f 2183 2225 2181 +f 2198 864 2192 +f 735 2193 2194 +f 861 886 2193 +f 2193 2192 861 +f 2193 886 2194 +f 2192 729 2198 +f 865 864 2198 +f 2198 2209 865 +f 866 865 2209 +f 771 2198 729 +f 2202 2201 2178 +f 2218 832 866 +f 767 765 2218 +f 767 2218 2209 +f 767 2209 771 +f 2198 771 2209 +f 2201 2217 2176 +f 2201 2176 2178 +f 672 831 765 +f 2217 2222 2172 +f 2222 2225 2183 +f 2222 2183 2172 +f 2226 2181 2225 +f 2226 2228 2179 +f 2275 2276 795 +f 2278 811 804 +f 2283 792 2301 +f 795 792 2283 +f 790 735 2301 +f 790 2301 792 +f 792 795 1901 +f 2304 2248 2240 +f 804 2276 2278 +f 735 2194 2301 +f 1920 792 1901 +f 790 792 1920 +f 790 1920 884 +f 2239 2302 2240 +f 804 795 2276 +f 2275 1898 2276 +f 720 2278 2147 +f 1899 795 804 +f 2278 2276 1887 +f 2147 2278 1887 +f 1898 1887 2276 +f 883 1919 2301 +f 795 2283 2275 +f 1899 1901 795 +f 884 887 790 +f 2283 1919 2275 +f 1898 2275 1900 +f 2283 2301 1919 +f 884 1920 883 +f 1919 883 1920 +f 735 790 887 +f 883 2301 886 +f 1900 2275 1919 +f 2165 2166 2263 +f 2295 2263 2166 +f 2194 886 2301 +f 2295 2302 2239 +f 2295 2239 2263 +f 2304 2240 2302 +f 2304 2305 2248 +f 2178 2248 2202 +f 2305 2202 2248 +# 808 faces + + #end of obj_0 + diff --git a/resources/qml/Account/AccountDetails.qml b/resources/qml/Account/AccountDetails.qml index 265842e2b4..031e376a21 100644 --- a/resources/qml/Account/AccountDetails.qml +++ b/resources/qml/Account/AccountDetails.qml @@ -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 QtQuick 2.10 @@ -7,19 +7,16 @@ import QtQuick.Controls 2.3 import UM 1.4 as UM import Cura 1.1 as Cura -Column +Item { - property var profile: null - property var loggedIn: false - property var profileImage: "" - - padding: UM.Theme.getSize("wide_margin").height - spacing: UM.Theme.getSize("wide_margin").height + property var profile: Cura.API.account.userProfile + property bool loggedIn: Cura.API.account.isLoggedIn + property var profileImage: Cura.API.account.profileImageUrl Loader { id: accountOperations - anchors.horizontalCenter: parent.horizontalCenter + anchors.centerIn: parent sourceComponent: loggedIn ? userOperations : generalOperations } diff --git a/resources/qml/Account/AccountWidget.qml b/resources/qml/Account/AccountWidget.qml index 26b491ce15..48c05f8a11 100644 --- a/resources/qml/Account/AccountWidget.qml +++ b/resources/qml/Account/AccountWidget.qml @@ -108,7 +108,15 @@ Item } } - onClicked: popup.opened ? popup.close() : popup.open() + onClicked: { + if (popup.opened) + { + popup.close() + } else { + Cura.API.account.popupOpened() + popup.open() + } + } } Popup @@ -119,17 +127,13 @@ Item x: parent.width - width closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + onOpened: Cura.API.account.popupOpened() opacity: opened ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 100 } } - + padding: 0 contentItem: AccountDetails - { - id: panel - profile: Cura.API.account.userProfile - loggedIn: Cura.API.account.isLoggedIn - profileImage: Cura.API.account.profileImageUrl - } + {} background: UM.PointingRectangle { diff --git a/resources/qml/Account/AvatarImage.qml b/resources/qml/Account/AvatarImage.qml index a4f922a10d..120173366f 100644 --- a/resources/qml/Account/AvatarImage.qml +++ b/resources/qml/Account/AvatarImage.qml @@ -54,6 +54,6 @@ Item visible: hasAvatar source: UM.Theme.getIcon("circle_outline") sourceSize: Qt.size(parent.width, parent.height) - color: UM.Theme.getColor("account_widget_ouline_active") + color: UM.Theme.getColor("account_widget_outline_active") } } diff --git a/resources/qml/Account/GeneralOperations.qml b/resources/qml/Account/GeneralOperations.qml index bea90c1d67..9562f940a4 100644 --- a/resources/qml/Account/GeneralOperations.qml +++ b/resources/qml/Account/GeneralOperations.qml @@ -10,7 +10,7 @@ import Cura 1.1 as Cura Column { spacing: UM.Theme.getSize("default_margin").width - + padding: UM.Theme.getSize("default_margin").width Image { id: machinesImage diff --git a/resources/qml/Account/SyncState.qml b/resources/qml/Account/SyncState.qml index 7126aec314..4e5543f751 100644 --- a/resources/qml/Account/SyncState.qml +++ b/resources/qml/Account/SyncState.qml @@ -4,27 +4,53 @@ import QtQuick.Controls 2.3 import UM 1.4 as UM import Cura 1.1 as Cura -Row // sync state icon + message +Row // Sync state icon + message { + property var syncState: Cura.API.account.syncState - property alias iconSource: icon.source - property alias labelText: stateLabel.text - property alias syncButtonVisible: accountSyncButton.visible - property alias animateIconRotation: updateAnimator.running - + id: syncRow width: childrenRect.width height: childrenRect.height - anchors.horizontalCenter: parent.horizontalCenter spacing: UM.Theme.getSize("narrow_margin").height + states: [ + State + { + name: "idle" + when: syncState == Cura.AccountSyncState.IDLE + PropertyChanges { target: icon; source: UM.Theme.getIcon("update")} + }, + State + { + name: "syncing" + when: syncState == Cura.AccountSyncState.SYNCING + PropertyChanges { target: icon; source: UM.Theme.getIcon("update") } + PropertyChanges { target: stateLabel; text: catalog.i18nc("@label", "Checking...")} + }, + State + { + name: "up_to_date" + when: syncState == Cura.AccountSyncState.SUCCESS + PropertyChanges { target: icon; source: UM.Theme.getIcon("checked") } + PropertyChanges { target: stateLabel; text: catalog.i18nc("@label", "Account synced")} + }, + State + { + name: "error" + when: syncState == Cura.AccountSyncState.ERROR + PropertyChanges { target: icon; source: UM.Theme.getIcon("warning_light") } + PropertyChanges { target: stateLabel; text: catalog.i18nc("@label", "Something went wrong...")} + } + ] + UM.RecolorImage { id: icon width: 20 * screenScaleFactor height: width - source: UM.Theme.getIcon("update") - color: palette.text + source: Cura.API.account.manualSyncEnabled ? UM.Theme.getIcon("update") : UM.Theme.getIcon("checked") + color: UM.Theme.getColor("account_sync_state_icon") RotationAnimator { @@ -34,7 +60,7 @@ Row // sync state icon + message to: 360 duration: 1000 loops: Animation.Infinite - running: true + running: syncState == Cura.AccountSyncState.SYNCING // reset rotation when stopped onRunningChanged: { @@ -54,10 +80,14 @@ Row // sync state icon + message Label { id: stateLabel - text: catalog.i18nc("@state", "Checking...") + text: catalog.i18nc("@state", catalog.i18nc("@label", "Account synced")) color: UM.Theme.getColor("text") font: UM.Theme.getFont("medium") renderType: Text.NativeRendering + width: contentWidth + UM.Theme.getSize("default_margin").height + height: contentHeight + verticalAlignment: Text.AlignVCenter + visible: !Cura.API.account.manualSyncEnabled } Label @@ -67,44 +97,19 @@ Row // sync state icon + message color: UM.Theme.getColor("secondary_button_text") font: UM.Theme.getFont("medium") renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + height: contentHeight + width: contentWidth + UM.Theme.getSize("default_margin").height + visible: Cura.API.account.manualSyncEnabled MouseArea { anchors.fill: parent - onClicked: Cura.API.account.sync() + onClicked: Cura.API.account.sync(true) hoverEnabled: true onEntered: accountSyncButton.font.underline = true onExited: accountSyncButton.font.underline = false } } } - - signal syncStateChanged(string newState) - - onSyncStateChanged: { - if(newState == Cura.AccountSyncState.SYNCING){ - syncRow.iconSource = UM.Theme.getIcon("update") - syncRow.labelText = catalog.i18nc("@label", "Checking...") - } else if (newState == Cura.AccountSyncState.SUCCESS) { - syncRow.iconSource = UM.Theme.getIcon("checked") - syncRow.labelText = catalog.i18nc("@label", "You are up to date") - } else if (newState == Cura.AccountSyncState.ERROR) { - syncRow.iconSource = UM.Theme.getIcon("warning_light") - syncRow.labelText = catalog.i18nc("@label", "Something went wrong...") - } else { - print("Error: unexpected sync state: " + newState) - } - - if(newState == Cura.AccountSyncState.SYNCING){ - syncRow.animateIconRotation = true - syncRow.syncButtonVisible = false - } else { - syncRow.animateIconRotation = false - syncRow.syncButtonVisible = true - } - } - - Component.onCompleted: Cura.API.account.syncStateChanged.connect(syncStateChanged) - - -} \ No newline at end of file +} diff --git a/resources/qml/Account/UserOperations.qml b/resources/qml/Account/UserOperations.qml index f292c501f3..e996e41b20 100644 --- a/resources/qml/Account/UserOperations.qml +++ b/resources/qml/Account/UserOperations.qml @@ -9,72 +9,119 @@ import Cura 1.1 as Cura Column { - width: Math.max(title.width, accountButton.width) + 2 * UM.Theme.getSize("default_margin").width + spacing: UM.Theme.getSize("narrow_margin").height + topPadding: UM.Theme.getSize("default_margin").height + bottomPadding: UM.Theme.getSize("default_margin").height + width: childrenRect.width - spacing: UM.Theme.getSize("default_margin").height - - SystemPalette + Item { - id: palette + id: accountInfo + width: childrenRect.width + height: childrenRect.height + anchors.left: parent.left + anchors.leftMargin: UM.Theme.getSize("default_margin").width + AvatarImage + { + id: avatar + anchors.verticalCenter: parent.verticalCenter + + width: UM.Theme.getSize("main_window_header").height + height: UM.Theme.getSize("main_window_header").height + + source: profile["profile_image_url"] ? profile["profile_image_url"] : "" + outlineColor: UM.Theme.getColor("main_background") + } + Rectangle + { + id: initialCircle + width: avatar.width + height: avatar.height + radius: width + anchors.verticalCenter: parent.verticalCenter + color: UM.Theme.getColor("action_button_disabled") + visible: !avatar.hasAvatar + Label + { + id: initialLabel + anchors.centerIn: parent + text: profile["username"].charAt(0).toUpperCase() + font: UM.Theme.getFont("large_bold") + color: UM.Theme.getColor("text") + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + renderType: Text.NativeRendering + } + } + + Column + { + anchors.left: avatar.right + anchors.leftMargin: UM.Theme.getSize("default_margin").width + spacing: UM.Theme.getSize("narrow_margin").height + width: childrenRect.width + height: childrenRect.height + Label + { + id: username + renderType: Text.NativeRendering + text: profile.username + font: UM.Theme.getFont("large_bold") + color: UM.Theme.getColor("text") + } + + SyncState + { + id: syncRow + } + Label + { + id: lastSyncLabel + renderType: Text.NativeRendering + text: catalog.i18nc("@label The argument is a timestamp", "Last update: %1").arg(Cura.API.account.lastSyncDateTime) + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text_medium") + } + } } - Label + Rectangle { - id: title - anchors.horizontalCenter: parent.horizontalCenter - horizontalAlignment: Text.AlignHCenter - renderType: Text.NativeRendering - text: catalog.i18nc("@label The argument is a username.", "Hi %1").arg(profile.username) - font: UM.Theme.getFont("large_bold") - color: UM.Theme.getColor("text") + width: parent.width + color: UM.Theme.getColor("lining") + height: UM.Theme.getSize("default_lining").height } - - SyncState + Cura.TertiaryButton { - id: syncRow - } - - - - Label - { - id: lastSyncLabel - anchors.horizontalCenter: parent.horizontalCenter - horizontalAlignment: Text.AlignHCenter - renderType: Text.NativeRendering - text: catalog.i18nc("@label The argument is a timestamp", "Last update: %1").arg(Cura.API.account.lastSyncDateTime) - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text_medium") - } - - Cura.SecondaryButton - { - id: accountButton - anchors.horizontalCenter: parent.horizontalCenter + id: cloudButton width: UM.Theme.getSize("account_button").width height: UM.Theme.getSize("account_button").height - text: catalog.i18nc("@button", "Ultimaker account") + text: catalog.i18nc("@button", "Ultimaker Digital Factory") + onClicked: Qt.openUrlExternally(CuraApplication.ultimakerDigitalFactoryUrl) + fixedWidthMode: false + } + + Cura.TertiaryButton + { + id: accountButton + width: UM.Theme.getSize("account_button").width + height: UM.Theme.getSize("account_button").height + text: catalog.i18nc("@button", "Ultimaker Account") onClicked: Qt.openUrlExternally(CuraApplication.ultimakerCloudAccountRootUrl) fixedWidthMode: false } - Label + Rectangle { - id: signOutButton - anchors.horizontalCenter: parent.horizontalCenter - text: catalog.i18nc("@button", "Sign out") - color: UM.Theme.getColor("secondary_button_text") - font: UM.Theme.getFont("medium") - renderType: Text.NativeRendering - - MouseArea - { - anchors.fill: parent - onClicked: Cura.API.account.logout() - hoverEnabled: true - onEntered: signOutButton.font.underline = true - onExited: signOutButton.font.underline = false - } + width: parent.width + color: UM.Theme.getColor("lining") + height: UM.Theme.getSize("default_lining").height } + Cura.TertiaryButton + { + id: signOutButton + onClicked: Cura.API.account.logout() + text: catalog.i18nc("@button", "Sign Out") + } } diff --git a/resources/qml/ActionButton.qml b/resources/qml/ActionButton.qml index d55c64029b..0c1be007b5 100644 --- a/resources/qml/ActionButton.qml +++ b/resources/qml/ActionButton.qml @@ -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 QtQuick 2.7 @@ -33,6 +33,8 @@ Button property alias shadowEnabled: shadow.visible property alias busy: busyIndicator.visible + property bool underlineTextOnHover: false + property alias toolTipContentAlignment: tooltip.contentAlignment // This property is used to indicate whether the button has a fixed width or the width would depend on the contents @@ -49,6 +51,14 @@ Button height: UM.Theme.getSize("action_button").height hoverEnabled: true + onHoveredChanged: + { + if(underlineTextOnHover) + { + buttonText.font.underline = hovered + } + } + contentItem: Row { spacing: UM.Theme.getSize("narrow_margin").width @@ -98,7 +108,7 @@ Button target: buttonText property: "width" value: button.fixedWidthMode ? button.width - button.leftPadding - button.rightPadding - : ((maximumWidth != 0 && contentWidth > maximumWidth) ? maximumWidth : undefined) + : ((maximumWidth != 0 && button.contentWidth > maximumWidth) ? maximumWidth : undefined) } } diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index ed2c6dc5fe..8ba651a5b0 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -84,6 +84,21 @@ UM.MainWindow CuraApplication.purgeWindows() } + Connections + { + // This connection is used when there is no ActiveMachine and the user is logged in + target: CuraApplication + onShowAddPrintersUncancellableDialog: + { + Cura.Actions.parent = backgroundItem + + // Reuse the welcome dialog item to show "Add a printer" only. + welcomeDialogItem.model = CuraApplication.getAddPrinterPagesModelWithoutCancel() + welcomeDialogItem.progressBarVisible = false + welcomeDialogItem.visible = true + } + } + Connections { target: CuraApplication @@ -117,6 +132,15 @@ UM.MainWindow welcomeDialogItem.progressBarVisible = false welcomeDialogItem.visible = true } + + // Reuse the welcome dialog item to show the "Add printers" dialog. Triggered when there is no active + // machine and the user is logged in. + if (!Cura.MachineManager.activeMachine && Cura.API.account.isLoggedIn) + { + welcomeDialogItem.model = CuraApplication.getAddPrinterPagesModelWithoutCancel() + welcomeDialogItem.progressBarVisible = false + welcomeDialogItem.visible = true + } } } diff --git a/resources/qml/Dialogs/DiscardOrKeepProfileChangesDialog.qml b/resources/qml/Dialogs/DiscardOrKeepProfileChangesDialog.qml index b7fe022d78..a5ee7b5986 100644 --- a/resources/qml/Dialogs/DiscardOrKeepProfileChangesDialog.qml +++ b/resources/qml/Dialogs/DiscardOrKeepProfileChangesDialog.qml @@ -14,8 +14,8 @@ UM.Dialog id: base title: catalog.i18nc("@title:window", "Discard or Keep changes") - width: UM.Theme.getSize("popup_dialog").width - height: UM.Theme.getSize("popup_dialog").height + minimumWidth: UM.Theme.getSize("popup_dialog").width + minimumHeight: UM.Theme.getSize("popup_dialog").height property var changesModel: Cura.UserChangesModel{ id: userChangesModel} onVisibilityChanged: { @@ -80,6 +80,8 @@ UM.Dialog property var extruder_name: userChangesModel.getItem(styleData.row).extruder anchors.left: parent.left anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.right: parent.right + elide: Text.ElideRight font: UM.Theme.getFont("system") text: { diff --git a/resources/qml/Dialogs/WorkspaceSummaryDialog.qml b/resources/qml/Dialogs/WorkspaceSummaryDialog.qml index 6fe9607274..670766204f 100644 --- a/resources/qml/Dialogs/WorkspaceSummaryDialog.qml +++ b/resources/qml/Dialogs/WorkspaceSummaryDialog.qml @@ -143,7 +143,7 @@ UM.Dialog { width: parent.width height: childrenRect.height - model: Cura.MachineManager.activeMachine.extruderList + model: Cura.MachineManager.activeMachine ? Cura.MachineManager.activeMachine.extruderList : null delegate: Column { height: childrenRect.height diff --git a/resources/qml/ExpandablePopup.qml b/resources/qml/ExpandablePopup.qml index 18255939ab..eb949a84ec 100644 --- a/resources/qml/ExpandablePopup.qml +++ b/resources/qml/ExpandablePopup.qml @@ -35,7 +35,8 @@ Item property color headerActiveColor: UM.Theme.getColor("secondary") property color headerHoverColor: UM.Theme.getColor("action_button_hovered") - property alias enabled: mouseArea.enabled + property alias mouseArea: headerMouseArea + property alias enabled: headerMouseArea.enabled // Text to show when this component is disabled property alias disabledText: disabledLabel.text @@ -139,6 +140,16 @@ Item anchors.fill: parent visible: base.enabled + MouseArea + { + id: headerMouseArea + anchors.fill: parent + onClicked: toggleContent() + hoverEnabled: true + onEntered: background.color = headerHoverColor + onExited: background.color = base.enabled ? headerBackgroundColor : UM.Theme.getColor("disabled") + } + Loader { id: headerItemLoader @@ -180,15 +191,6 @@ Item } } - MouseArea - { - id: mouseArea - anchors.fill: parent - onClicked: toggleContent() - hoverEnabled: true - onEntered: background.color = headerHoverColor - onExited: background.color = base.enabled ? headerBackgroundColor : UM.Theme.getColor("disabled") - } } DropShadow diff --git a/resources/qml/MachineSettings/NumericTextFieldWithUnit.qml b/resources/qml/MachineSettings/NumericTextFieldWithUnit.qml index a4ff27391f..031ef5241a 100644 --- a/resources/qml/MachineSettings/NumericTextFieldWithUnit.qml +++ b/resources/qml/MachineSettings/NumericTextFieldWithUnit.qml @@ -77,6 +77,8 @@ UM.TooltipArea anchors.left: fieldLabel.right anchors.leftMargin: UM.Theme.getSize("default_margin").width verticalAlignment: Text.AlignVCenter + padding: 0 + leftPadding: UM.Theme.getSize("narrow_margin").width width: numericTextFieldWithUnit.controlWidth height: numericTextFieldWithUnit.controlHeight diff --git a/resources/qml/MainWindow/ApplicationMenu.qml b/resources/qml/MainWindow/ApplicationMenu.qml index 3204787c6b..4e08cb08dd 100644 --- a/resources/qml/MainWindow/ApplicationMenu.qml +++ b/resources/qml/MainWindow/ApplicationMenu.qml @@ -35,6 +35,7 @@ Item MenuSeparator { } MenuItem { action: Cura.Actions.selectAll } MenuItem { action: Cura.Actions.arrangeAll } + MenuItem { action: Cura.Actions.multiplySelection } MenuItem { action: Cura.Actions.deleteSelection } MenuItem { action: Cura.Actions.deleteAll } MenuItem { action: Cura.Actions.resetAllTranslation } diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml index b47d77243c..b68d37d1ea 100644 --- a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml @@ -96,7 +96,7 @@ Item id: configurationList spacing: UM.Theme.getSize("narrow_margin").height width: container.width - ((height > container.maximumHeight) ? container.ScrollBar.vertical.background.width : 0) //Make room for scroll bar if there is any. - height: childrenRect.height + height: contentHeight interactive: false // let the ScrollView process scroll events. section.property: "modelData.printerType" diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml index f0ada92810..a499242c94 100644 --- a/resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml @@ -4,6 +4,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 import QtQuick.Controls.Styles 1.4 +import QtQuick.Layouts 1.3 import UM 1.2 as UM import Cura 1.0 as Cura @@ -32,75 +33,74 @@ Cura.ExpandablePopup } contentPadding: UM.Theme.getSize("default_lining").width - enabled: Cura.MachineManager.activeMachine.hasMaterials || Cura.MachineManager.activeMachine.hasVariants || Cura.MachineManager.activeMachine.hasVariantBuildplates; //Only let it drop down if there is any configuration that you could change. + enabled: Cura.MachineManager.activeMachine ? Cura.MachineManager.activeMachine.hasMaterials || Cura.MachineManager.activeMachine.hasVariants || Cura.MachineManager.activeMachine.hasVariantBuildplates : false; //Only let it drop down if there is any configuration that you could change. headerItem: Item { // Horizontal list that shows the extruders and their materials - ListView + RowLayout { - id: extrudersList - - orientation: ListView.Horizontal anchors.fill: parent - model: extrudersModel - visible: Cura.MachineManager.activeMachine.hasMaterials - - delegate: Item + visible: Cura.MachineManager.activeMachine ? Cura.MachineManager.activeMachine.hasMaterials : false + Repeater { - height: parent.height - width: Math.round(ListView.view.width / extrudersModel.count) - - // Extruder icon. Shows extruder index and has the same color as the active material. - Cura.ExtruderIcon + model: extrudersModel + delegate: Item { - id: extruderIcon - materialColor: model.color - extruderEnabled: model.enabled - height: parent.height - width: height - } + Layout.preferredWidth: Math.round(parent.width / extrudersModel.count) + Layout.fillHeight: true - // Label for the brand of the material - Label - { - id: typeAndBrandNameLabel - - text: model.material_brand + " " + model.material - elide: Text.ElideRight - font: UM.Theme.getFont("default") - color: UM.Theme.getColor("text") - renderType: Text.NativeRendering - - anchors + // Extruder icon. Shows extruder index and has the same color as the active material. + Cura.ExtruderIcon { - top: extruderIcon.top - left: extruderIcon.right - leftMargin: UM.Theme.getSize("default_margin").width - right: parent.right - rightMargin: UM.Theme.getSize("default_margin").width + id: extruderIcon + materialColor: model.color + extruderEnabled: model.enabled + height: parent.height + width: height } - } - // Label that shows the name of the variant - Label - { - id: variantLabel - visible: Cura.MachineManager.activeMachine.hasVariants - - text: model.variant - elide: Text.ElideRight - font: UM.Theme.getFont("default_bold") - color: UM.Theme.getColor("text") - renderType: Text.NativeRendering - - anchors + // Label for the brand of the material + Label { - left: extruderIcon.right - leftMargin: UM.Theme.getSize("default_margin").width - top: typeAndBrandNameLabel.bottom - right: parent.right - rightMargin: UM.Theme.getSize("default_margin").width + id: typeAndBrandNameLabel + + text: model.material_brand + " " + model.material + elide: Text.ElideRight + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + renderType: Text.NativeRendering + + anchors + { + top: extruderIcon.top + left: extruderIcon.right + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + rightMargin: UM.Theme.getSize("default_margin").width + } + } + // Label that shows the name of the variant + Label + { + id: variantLabel + + visible: Cura.MachineManager.activeMachine ? Cura.MachineManager.activeMachine.hasVariants : false + + text: model.variant + elide: Text.ElideRight + font: UM.Theme.getFont("default_bold") + color: UM.Theme.getColor("text") + renderType: Text.NativeRendering + + anchors + { + left: extruderIcon.right + leftMargin: UM.Theme.getSize("default_margin").width + top: typeAndBrandNameLabel.bottom + right: parent.right + rightMargin: UM.Theme.getSize("default_margin").width + } } } } @@ -115,7 +115,7 @@ Cura.ExpandablePopup color: UM.Theme.getColor("text") renderType: Text.NativeRendering - visible: !Cura.MachineManager.activeMachine.hasMaterials && (Cura.MachineManager.activeMachine.hasVariants || Cura.MachineManager.activeMachine.hasVariantBuildplates) + visible: Cura.MachineManager.activeMachine ? !Cura.MachineManager.activeMachine.hasMaterials && (Cura.MachineManager.activeMachine.hasVariants || Cura.MachineManager.activeMachine.hasVariantBuildplates) : false anchors { diff --git a/resources/qml/Menus/ConfigurationMenu/CustomConfiguration.qml b/resources/qml/Menus/ConfigurationMenu/CustomConfiguration.qml index 65f5bcce8c..010e2e77b0 100644 --- a/resources/qml/Menus/ConfigurationMenu/CustomConfiguration.qml +++ b/resources/qml/Menus/ConfigurationMenu/CustomConfiguration.qml @@ -244,7 +244,7 @@ Item Row { height: visible ? UM.Theme.getSize("print_setup_big_item").height : 0 - visible: Cura.MachineManager.activeMachine.hasMaterials + visible: Cura.MachineManager.activeMachine ? Cura.MachineManager.activeMachine.hasMaterials : false Label { @@ -305,7 +305,7 @@ Item Row { height: visible ? UM.Theme.getSize("print_setup_big_item").height : 0 - visible: Cura.MachineManager.activeMachine.hasVariants + visible: Cura.MachineManager.activeMachine ? Cura.MachineManager.activeMachine.hasVariants : false Label { diff --git a/resources/qml/Menus/RecentFilesMenu.qml b/resources/qml/Menus/RecentFilesMenu.qml index b788b5e72e..9de523280c 100644 --- a/resources/qml/Menus/RecentFilesMenu.qml +++ b/resources/qml/Menus/RecentFilesMenu.qml @@ -24,7 +24,7 @@ Menu { text: { - var path = modelData.toString() + var path = decodeURIComponent(modelData.toString()) return (index + 1) + ". " + path.slice(path.lastIndexOf("/") + 1); } onTriggered: diff --git a/resources/qml/Preferences/GeneralPage.qml b/resources/qml/Preferences/GeneralPage.qml index 404c961a90..8839bf4a05 100644 --- a/resources/qml/Preferences/GeneralPage.qml +++ b/resources/qml/Preferences/GeneralPage.qml @@ -72,6 +72,9 @@ UM.PreferencesPage var defaultTheme = UM.Preferences.getValue("general/theme") setDefaultTheme(defaultTheme) + UM.Preferences.resetPreference("cura/single_instance") + singleInstanceCheckbox.checked = boolCheck(UM.Preferences.getValue("cura/single_instance")) + UM.Preferences.resetPreference("physics/automatic_push_free") pushFreeCheckbox.checked = boolCheck(UM.Preferences.getValue("physics/automatic_push_free")) UM.Preferences.resetPreference("physics/automatic_drop_down") @@ -154,7 +157,7 @@ UM.PreferencesPage Component.onCompleted: { append({ text: "English", code: "en_US" }) - append({ text: "Czech", code: "cs_CZ" }) + append({ text: "Čeština", code: "cs_CZ" }) append({ text: "Deutsch", code: "de_DE" }) append({ text: "Español", code: "es_ES" }) //Finnish is disabled for being incomplete: append({ text: "Suomi", code: "fi_FI" }) @@ -560,6 +563,21 @@ UM.PreferencesPage text: catalog.i18nc("@label","Opening and saving files") } + UM.TooltipArea + { + width: childrenRect.width + height: childrenRect.height + text: catalog.i18nc("@info:tooltip","Should opening files from the desktop or external applications open in the same instance of Cura?") + + CheckBox + { + id: singleInstanceCheckbox + text: catalog.i18nc("@option:check","Use a single instance of Cura") + checked: boolCheck(UM.Preferences.getValue("cura/single_instance")) + onCheckedChanged: UM.Preferences.setValue("cura/single_instance", checked) + } + } + UM.TooltipArea { width: childrenRect.width diff --git a/resources/qml/Preferences/MachinesPage.qml b/resources/qml/Preferences/MachinesPage.qml index a3a8ec0e29..5341af65db 100644 --- a/resources/qml/Preferences/MachinesPage.qml +++ b/resources/qml/Preferences/MachinesPage.qml @@ -136,6 +136,7 @@ UM.ManagementPage { id: confirmDialog object: base.currentItem && base.currentItem.name ? base.currentItem.name : "" + text: base.currentItem ? base.currentItem.removalWarning : ""; onYes: { Cura.MachineManager.removeMachine(base.currentItem.id) diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml index 19c2562874..9274bf80ad 100644 --- a/resources/qml/PrintMonitor.qml +++ b/resources/qml/PrintMonitor.qml @@ -77,7 +77,7 @@ Item Repeater { id: extrudersRepeater - model: activePrinter != null ? activePrinter.extruders : null + model: activePrinter != null ? activePrinter.extruderList : null ExtruderBox { diff --git a/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml b/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml index 46297659ff..db19ed89df 100644 --- a/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml +++ b/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml @@ -73,8 +73,6 @@ Item height: textLabel.contentHeight + 2 * UM.Theme.getSize("narrow_margin").height hoverEnabled: true - baselineOffset: null // If we don't do this, there is a binding loop. WHich is a bit weird, since we override the contentItem anyway... - contentItem: RowLayout { spacing: 0 diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedSupportSelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedSupportSelector.qml index f227dddaf9..92f0024b23 100644 --- a/resources/qml/PrintSetupSelector/Recommended/RecommendedSupportSelector.qml +++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedSupportSelector.qml @@ -130,7 +130,11 @@ Item target: extruderModel onModelChanged: { - supportExtruderCombobox.color = supportExtruderCombobox.model.getItem(supportExtruderCombobox.currentIndex).color + var maybeColor = supportExtruderCombobox.model.getItem(supportExtruderCombobox.currentIndex).color + if (maybeColor) + { + supportExtruderCombobox.color = maybeColor + } } } onCurrentIndexChanged: diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml index f2dc791651..ddd0a87c9f 100644 --- a/resources/qml/PrinterOutput/ExtruderBox.qml +++ b/resources/qml/PrinterOutput/ExtruderBox.qml @@ -38,7 +38,7 @@ Item Label //Extruder name. { - text: Cura.MachineManager.activeMachine.extruders[position].name !== "" ? Cura.MachineManager.activeMachine.extruders[position].name : catalog.i18nc("@label", "Extruder") + text: Cura.MachineManager.activeMachine.extruderList[position].name !== "" ? Cura.MachineManager.activeMachine.extruderList[position].name : catalog.i18nc("@label", "Extruder") color: UM.Theme.getColor("text") font: UM.Theme.getFont("default") anchors.left: parent.left diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index 2a101e4ae3..0907767eea 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -5,16 +5,67 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 import UM 1.2 as UM -import Cura 1.0 as Cura +import Cura 1.1 as Cura Cura.ExpandablePopup { id: machineSelector property bool isNetworkPrinter: Cura.MachineManager.activeMachineHasNetworkConnection - property bool isCloudPrinter: Cura.MachineManager.activeMachineHasCloudConnection + property bool isConnectedCloudPrinter: Cura.MachineManager.activeMachineHasCloudConnection + property bool isCloudRegistered: Cura.MachineManager.activeMachineHasCloudRegistration property bool isGroup: Cura.MachineManager.activeMachineIsGroup + readonly property string connectionStatus: { + if (isNetworkPrinter) + { + return "printer_connected" + } + else if (isConnectedCloudPrinter && Cura.API.connectionStatus.isInternetReachable) + { + return "printer_cloud_connected" + } + else if (isCloudRegistered) + { + return "printer_cloud_not_available" + } + else + { + return "" + } + } + + function getConnectionStatusMessage() { + if (connectionStatus == "printer_cloud_not_available") + { + if(Cura.API.connectionStatus.isInternetReachable) + { + if (Cura.API.account.isLoggedIn) + { + if (Cura.MachineManager.activeMachineIsLinkedToCurrentAccount) + { + return catalog.i18nc("@status", "The cloud printer is offline. Please check if the printer is turned on and connected to the internet.") + } + else + { + return catalog.i18nc("@status", "This printer is not linked to your account. Please visit the Ultimaker Digital Factory to establish a connection.") + } + } + else + { + return catalog.i18nc("@status", "The cloud connection is currently unavailable. Please sign in to connect to the cloud printer.") + } + } else + { + return catalog.i18nc("@status", "The cloud connection is currently unavailable. Please check your internet connection.") + } + } + else + { + return "" + } + } + contentPadding: UM.Theme.getSize("default_lining").width contentAlignment: Cura.ExpandablePopup.ContentAlignment.AlignLeft @@ -44,7 +95,7 @@ Cura.ExpandablePopup { return UM.Theme.getIcon("printer_group") } - else if (isNetworkPrinter || isCloudPrinter) + else if (isNetworkPrinter || isCloudRegistered) { return UM.Theme.getIcon("printer_single") } @@ -59,6 +110,7 @@ Cura.ExpandablePopup UM.RecolorImage { + id: connectionStatusImage anchors { bottom: parent.bottom @@ -66,27 +118,14 @@ Cura.ExpandablePopup leftMargin: UM.Theme.getSize("thick_margin").width } - source: - { - if (isNetworkPrinter) - { - return UM.Theme.getIcon("printer_connected") - } - else if (isCloudPrinter) - { - return UM.Theme.getIcon("printer_cloud_connected") - } - else - { - return "" - } - } + source: UM.Theme.getIcon(connectionStatus) width: UM.Theme.getSize("printer_status_icon").width height: UM.Theme.getSize("printer_status_icon").height - color: UM.Theme.getColor("primary") - visible: isNetworkPrinter || isCloudPrinter + color: connectionStatus == "printer_cloud_not_available" ? UM.Theme.getColor("cloud_unavailable") : UM.Theme.getColor("primary") + + visible: isNetworkPrinter || isCloudRegistered // Make a themable circle in the background so we can change it in other themes Rectangle @@ -100,6 +139,39 @@ Cura.ExpandablePopup color: UM.Theme.getColor("main_background") z: parent.z - 1 } + + } + + MouseArea // Connection status tooltip hover area + { + id: connectionStatusTooltipHoverArea + anchors.fill: parent + hoverEnabled: getConnectionStatusMessage() !== "" + acceptedButtons: Qt.NoButton // react to hover only, don't steal clicks + + onEntered: + { + machineSelector.mouseArea.entered() // we want both this and the outer area to be entered + tooltip.tooltipText = getConnectionStatusMessage() + tooltip.show() + } + onExited: { tooltip.hide() } + } + + Cura.ToolTip + { + id: tooltip + + width: 250 * screenScaleFactor + tooltipText: getConnectionStatusMessage() + arrowSize: UM.Theme.getSize("button_tooltip_arrow").width + x: connectionStatusImage.x - UM.Theme.getSize("narrow_margin").width + y: connectionStatusImage.y + connectionStatusImage.height + UM.Theme.getSize("narrow_margin").height + z: popup.z + 1 + targetPoint: Qt.point( + connectionStatusImage.x + Math.round(connectionStatusImage.width / 2), + connectionStatusImage.y + ) } } diff --git a/resources/qml/PrinterSelector/MachineSelectorList.qml b/resources/qml/PrinterSelector/MachineSelectorList.qml index a7c041630f..18b1a68b20 100644 --- a/resources/qml/PrinterSelector/MachineSelectorList.qml +++ b/resources/qml/PrinterSelector/MachineSelectorList.qml @@ -28,11 +28,11 @@ ListView delegate: MachineSelectorButton { - text: model.name + text: model.name ? model.name : "" width: listView.width outputDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null - checked: Cura.MachineManager.activeMachine.id == model.id + checked: Cura.MachineManager.activeMachine ? Cura.MachineManager.activeMachine.id == model.id : false onClicked: { diff --git a/resources/qml/TertiaryButton.qml b/resources/qml/TertiaryButton.qml new file mode 100644 index 0000000000..e3840dbdd3 --- /dev/null +++ b/resources/qml/TertiaryButton.qml @@ -0,0 +1,21 @@ +// Copyright (c) 2020 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.2 + +import UM 1.4 as UM +import Cura 1.1 as Cura + + +Cura.ActionButton +{ + shadowEnabled: true + shadowColor: enabled ? UM.Theme.getColor("secondary_button_shadow"): UM.Theme.getColor("action_button_disabled_shadow") + color: "transparent" + textColor: UM.Theme.getColor("secondary_button_text") + outlineColor: "transparent" + disabledColor: UM.Theme.getColor("action_button_disabled") + textDisabledColor: UM.Theme.getColor("action_button_disabled_text") + hoverColor: "transparent" + underlineTextOnHover: true +} diff --git a/resources/qml/ToolTip.qml b/resources/qml/ToolTip.qml index e82caf01b2..ad58038d01 100644 --- a/resources/qml/ToolTip.qml +++ b/resources/qml/ToolTip.qml @@ -19,12 +19,20 @@ ToolTip property int contentAlignment: Cura.ToolTip.ContentAlignment.AlignRight property alias tooltipText: tooltip.text + property alias arrowSize: backgroundRect.arrowSize property var targetPoint: Qt.point(parent.x, y + Math.round(height/2)) id: tooltip text: "" delay: 500 font: UM.Theme.getFont("default") + visible: opacity != 0.0 + opacity: 0.0 // initially hidden + + Behavior on opacity + { + NumberAnimation { duration: 100; } + } // If the text is not set, just set the height to 0 to prevent it from showing height: text != "" ? label.contentHeight + 2 * UM.Theme.getSize("thin_margin").width: 0 @@ -60,4 +68,12 @@ ToolTip color: UM.Theme.getColor("tooltip_text") renderType: Text.NativeRendering } + + function show() { + opacity = 1 + } + + function hide() { + opacity = 0 + } } \ No newline at end of file diff --git a/resources/qml/Toolbar.qml b/resources/qml/Toolbar.qml index 2e12f720a5..0bf09b4d18 100644 --- a/resources/qml/Toolbar.qml +++ b/resources/qml/Toolbar.qml @@ -96,6 +96,8 @@ Item { UM.Controller.setActiveTool(model.id); } + + base.state = (index < toolsModel.count/2) ? "anchorAtTop" : "anchorAtBottom"; } } } @@ -129,7 +131,6 @@ Item Repeater { - id: extruders width: childrenRect.width height: childrenRect.height model: extrudersModel.items.length > 1 ? extrudersModel : 0 @@ -220,4 +221,40 @@ Item visible: toolHint.text != "" } + + states: [ + State { + name: "anchorAtTop" + + AnchorChanges { + target: panelBorder + anchors.top: base.top + anchors.bottom: undefined + } + PropertyChanges { + target: panelBorder + anchors.topMargin: base.activeY + } + }, + State { + name: "anchorAtBottom" + + AnchorChanges { + target: panelBorder + anchors.top: undefined + anchors.bottom: base.top + } + PropertyChanges { + target: panelBorder + anchors.bottomMargin: { + if (panelBorder.height > (base.activeY + UM.Theme.getSize("button").height)) { + // panel is tall, align the top of the panel with the top of the first tool button + return -panelBorder.height + } + // align the bottom of the panel with the bottom of the selected tool button + return -(base.activeY + UM.Theme.getSize("button").height) + } + } + } + ] } diff --git a/resources/qml/WelcomePages/AddLocalPrinterScrollView.qml b/resources/qml/WelcomePages/AddLocalPrinterScrollView.qml index 50ceeff8a9..cecbf5b4ed 100644 --- a/resources/qml/WelcomePages/AddLocalPrinterScrollView.qml +++ b/resources/qml/WelcomePages/AddLocalPrinterScrollView.qml @@ -5,7 +5,7 @@ import QtQuick 2.10 import QtQuick.Controls 2.3 import UM 1.3 as UM -import Cura 1.0 as Cura +import Cura 1.1 as Cura // @@ -29,8 +29,6 @@ Item "Custom": -1 } - property int maxItemCountAtOnce: 10 // show at max 10 items at once, otherwise you need to scroll. - // User-editable printer name property alias printerName: printerNameTextField.text property alias isPrinterNameValid: printerNameTextField.acceptableInput @@ -54,12 +52,27 @@ Item } } + function getMachineName() + { + return machineList.model.getItem(machineList.currentIndex) != undefined ? machineList.model.getItem(machineList.currentIndex).name : ""; + } + + function getMachineMetaDataEntry(key) + { + var metadata = machineList.model.getItem(machineList.currentIndex) != undefined ? machineList.model.getItem(machineList.currentIndex).metadata : undefined; + if (metadata) + { + return metadata[key]; + } + return undefined; + } + Component.onCompleted: { updateCurrentItemUponSectionChange() } - Item + Row { id: localPrinterSelectionItem anchors.left: parent.left @@ -68,19 +81,12 @@ Item height: childrenRect.height // ScrollView + ListView for selecting a local printer to add - ScrollView + Cura.ScrollView { id: scrollView - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top - height: (maxItemCountAtOnce * UM.Theme.getSize("action_button").height) - UM.Theme.getSize("default_margin").height - - ScrollBar.horizontal.policy: ScrollBar.AlwaysOff - ScrollBar.vertical.policy: ScrollBar.AsNeeded - - clip: true + height: childrenHeight + width: Math.floor(parent.width * 0.4) ListView { @@ -183,52 +189,94 @@ Item } } } - } - // Horizontal line - Rectangle - { - id: horizontalLine - anchors.top: localPrinterSelectionItem.bottom - anchors.left: parent.left - anchors.right: parent.right - height: UM.Theme.getSize("default_lining").height - color: UM.Theme.getColor("lining") - } - - // User-editable printer name row - Row - { - anchors.top: horizontalLine.bottom - anchors.left: parent.left - anchors.right: parent.right - anchors.topMargin: UM.Theme.getSize("default_lining").height - anchors.leftMargin: UM.Theme.getSize("default_margin").width - - spacing: UM.Theme.getSize("default_margin").width - - Label + // Vertical line + Rectangle { - text: catalog.i18nc("@label", "Printer name") - anchors.verticalCenter: parent.verticalCenter - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("text") - verticalAlignment: Text.AlignVCenter - renderType: Text.NativeRendering + id: verticalLine + anchors.top: parent.top + height: childrenHeight - UM.Theme.getSize("default_lining").height + width: UM.Theme.getSize("default_lining").height + color: UM.Theme.getColor("lining") } - Cura.TextField + // User-editable printer name row + Column { - id: printerNameTextField - anchors.verticalCenter: parent.verticalCenter - width: (parent.width / 2) | 0 - placeholderText: catalog.i18nc("@text", "Please give your printer a name") - maximumLength: 40 - validator: RegExpValidator + width: Math.floor(parent.width * 0.6) + + spacing: UM.Theme.getSize("default_margin").width + padding: UM.Theme.getSize("default_margin").width + + Label { - regExp: printerNameTextField.machineNameValidator.machineNameRegex + width: parent.width + wrapMode: Text.WordWrap + text: base.getMachineName() + color: UM.Theme.getColor("primary_button") + font: UM.Theme.getFont("huge") + elide: Text.ElideRight + } + Grid + { + width: parent.width + columns: 2 + rowSpacing: UM.Theme.getSize("default_lining").height + columnSpacing: UM.Theme.getSize("default_margin").width + + verticalItemAlignment: Grid.AlignVCenter + + Label + { + text: catalog.i18nc("@label", "Manufacturer") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + renderType: Text.NativeRendering + } + Label + { + text: base.getMachineMetaDataEntry("manufacturer") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + renderType: Text.NativeRendering + } + Label + { + text: catalog.i18nc("@label", "Profile author") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + renderType: Text.NativeRendering + } + Label + { + text: base.getMachineMetaDataEntry("author") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + renderType: Text.NativeRendering + } + + Label + { + text: catalog.i18nc("@label", "Printer name") + font: UM.Theme.getFont("default") + color: UM.Theme.getColor("text") + renderType: Text.NativeRendering + } + + Cura.TextField + { + id: printerNameTextField + placeholderText: catalog.i18nc("@text", "Please give your printer a name") + maximumLength: 40 + validator: RegExpValidator + { + regExp: printerNameTextField.machineNameValidator.machineNameRegex + } + property var machineNameValidator: Cura.MachineNameValidator { } + } } - property var machineNameValidator: Cura.MachineNameValidator { } } + + } } diff --git a/resources/qml/WelcomePages/AddNetworkOrLocalPrinterContent.qml b/resources/qml/WelcomePages/AddNetworkOrLocalPrinterContent.qml index b6f715aa0b..6ac567b0b1 100644 --- a/resources/qml/WelcomePages/AddNetworkOrLocalPrinterContent.qml +++ b/resources/qml/WelcomePages/AddNetworkOrLocalPrinterContent.qml @@ -108,6 +108,12 @@ Item AddLocalPrinterScrollView { id: localPrinterView + property int childrenHeight: backButton.y - addLocalPrinterDropDown.y - UM.Theme.getSize("expandable_component_content_header").height - UM.Theme.getSize("default_margin").height + + onChildrenHeightChanged: + { + addLocalPrinterDropDown.children[1].height = childrenHeight + } } } } diff --git a/resources/qml/Widgets/ScrollView.qml b/resources/qml/Widgets/ScrollView.qml new file mode 100644 index 0000000000..9e7531994c --- /dev/null +++ b/resources/qml/Widgets/ScrollView.qml @@ -0,0 +1,46 @@ +// Copyright (c) 2020 Ultimaker B.V. +// Toolbox is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.10 +import QtQuick.Controls 2.3 + +import UM 1.1 as UM + +ScrollView +{ + clip: true + + // Setting this property to false hides the scrollbar both when the scrollbar is not needed (child height < height) + // and when the scrollbar is not actively being hovered or pressed + property bool scrollAlwaysVisible: true + + ScrollBar.vertical: ScrollBar + { + hoverEnabled: true + policy: parent.scrollAlwaysVisible ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded + anchors.top: parent.top + anchors.right: parent.right + anchors.bottom: parent.bottom + + contentItem: Rectangle + { + implicitWidth: UM.Theme.getSize("scrollbar").width + opacity: (parent.active || parent.parent.scrollAlwaysVisible) ? 1.0 : 0.0 + radius: Math.round(width / 2) + color: + { + if (parent.pressed) + { + return UM.Theme.getColor("scrollbar_handle_down") + } + else if (parent.hovered) + { + return UM.Theme.getColor("scrollbar_handle_hover") + } + return UM.Theme.getColor("scrollbar_handle") + } + Behavior on color { ColorAnimation { duration: 100; } } + Behavior on opacity { NumberAnimation { duration: 100 } } + } + } +} \ No newline at end of file diff --git a/resources/qml/qmldir b/resources/qml/qmldir index dcc2e410c9..ab61101778 100644 --- a/resources/qml/qmldir +++ b/resources/qml/qmldir @@ -35,6 +35,7 @@ RadioButton 1.0 RadioButton.qml Scrollable 1.0 Scrollable.qml TabButton 1.0 TabButton.qml TextField 1.0 TextField.qml +ScrollView 1.0 ScrollView.qml # Cura/MachineSettings diff --git a/resources/quality/atmat_signal_pro/signal_pro_global_extrafast_quality.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_global_extrafast_quality.inst.cfg new file mode 100644 index 0000000000..348f021cc4 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_global_extrafast_quality.inst.cfg @@ -0,0 +1,14 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +global_quality = True + +[values] +layer_height = 0.3 + diff --git a/resources/quality/atmat_signal_pro/signal_pro_global_extrafine_quality.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_global_extrafine_quality.inst.cfg new file mode 100644 index 0000000000..640b40578e --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_global_extrafine_quality.inst.cfg @@ -0,0 +1,14 @@ +[general] +version = 4 +name = Extra Fine +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafine +global_quality = True + +[values] +layer_height = 0.06 + diff --git a/resources/quality/atmat_signal_pro/signal_pro_global_fast_quality.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_global_fast_quality.inst.cfg new file mode 100644 index 0000000000..059682d713 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_global_fast_quality.inst.cfg @@ -0,0 +1,14 @@ +[general] +version = 4 +name = Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fast +global_quality = True + +[values] +layer_height = 0.2 + diff --git a/resources/quality/atmat_signal_pro/signal_pro_global_fine_quality.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_global_fine_quality.inst.cfg new file mode 100644 index 0000000000..01803ab313 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_global_fine_quality.inst.cfg @@ -0,0 +1,14 @@ +[general] +version = 4 +name = Fine +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fine +global_quality = True + +[values] +layer_height = 0.1 + diff --git a/resources/quality/atmat_signal_pro/signal_pro_global_normal_quality.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_global_normal_quality.inst.cfg new file mode 100644 index 0000000000..958fc7f9a5 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_global_normal_quality.inst.cfg @@ -0,0 +1,14 @@ +[general] +version = 4 +name = Normal +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +global_quality = True + +[values] +layer_height = 0.15 + diff --git a/resources/quality/atmat_signal_pro/signal_pro_global_sprint_quality.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_global_sprint_quality.inst.cfg new file mode 100644 index 0000000000..ee26c7509b --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_global_sprint_quality.inst.cfg @@ -0,0 +1,14 @@ +[general] +version = 4 +name = Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = sprint +global_quality = True + +[values] +layer_height = 0.4 + diff --git a/resources/quality/atmat_signal_pro/signal_pro_global_supersprint_quality.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_global_supersprint_quality.inst.cfg new file mode 100644 index 0000000000..ee143d09ea --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_global_supersprint_quality.inst.cfg @@ -0,0 +1,14 @@ +[general] +version = 4 +name = Super Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = supersprint +global_quality = True + +[values] +layer_height = 0.5 + diff --git a/resources/quality/atmat_signal_pro/signal_pro_global_ultrasprint_quality.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_global_ultrasprint_quality.inst.cfg new file mode 100644 index 0000000000..38687e27de --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_global_ultrasprint_quality.inst.cfg @@ -0,0 +1,14 @@ +[general] +version = 4 +name = Ultra Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = ultrasprint +global_quality = True + +[values] +layer_height = 0.6 + diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_ABS_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_ABS_extrafast.inst.cfg new file mode 100644 index 0000000000..91f2dc8411 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_ABS_extrafast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_abs_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 25 +build_volume_temperature = 40 +speed_print = 50 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_ABS_fast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_ABS_fast.inst.cfg new file mode 100644 index 0000000000..60b19adfa3 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_ABS_fast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fast +material = generic_abs_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 25 +build_volume_temperature = 40 +speed_print = 50 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_ABS_fine.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_ABS_fine.inst.cfg new file mode 100644 index 0000000000..73367a9752 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_ABS_fine.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Fine +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fine +material = generic_abs_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 25 +build_volume_temperature = 40 +speed_print = 50 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_ABS_normal.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_ABS_normal.inst.cfg new file mode 100644 index 0000000000..6fec9f3d7d --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_ABS_normal.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Normal +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +material = generic_abs_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 25 +build_volume_temperature = 40 +speed_print = 50 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_HIPS_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_HIPS_extrafast.inst.cfg new file mode 100644 index 0000000000..edfce26491 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_HIPS_extrafast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_hips_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 25 +material_print_temperature = =default_material_print_temperature + 50 +build_volume_temperature = 40 +speed_print = 40 +retraction_amount = 1 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_HIPS_fast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_HIPS_fast.inst.cfg new file mode 100644 index 0000000000..e822848f76 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_HIPS_fast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fast +material = generic_hips_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 25 +material_print_temperature = =default_material_print_temperature + 50 +build_volume_temperature = 40 +speed_print = 40 +retraction_amount = 1 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_HIPS_fine.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_HIPS_fine.inst.cfg new file mode 100644 index 0000000000..3b8e3b1006 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_HIPS_fine.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Fine +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fine +material = generic_hips_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 25 +material_print_temperature = =default_material_print_temperature + 50 +build_volume_temperature = 40 +speed_print = 40 +retraction_amount = 1 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_HIPS_normal.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_HIPS_normal.inst.cfg new file mode 100644 index 0000000000..f1efcf936c --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_HIPS_normal.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Normal +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +material = generic_hips_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 25 +material_print_temperature = =default_material_print_temperature + 50 +build_volume_temperature = 40 +speed_print = 40 +retraction_amount = 1 +cool_fan_enabled = False diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PA_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PA_extrafast.inst.cfg new file mode 100644 index 0000000000..04356023b5 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PA_extrafast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_nylon_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 10 +material_print_temperature = =default_material_print_temperature + 20 +build_volume_temperature = 40 +speed_print = 30 +retraction_amount = 1 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PA_fast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PA_fast.inst.cfg new file mode 100644 index 0000000000..debcb3bd89 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PA_fast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fast +material = generic_nylon_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 10 +material_print_temperature = =default_material_print_temperature + 20 +build_volume_temperature = 40 +speed_print = 30 +retraction_amount = 1 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PA_fine.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PA_fine.inst.cfg new file mode 100644 index 0000000000..112c5c61b7 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PA_fine.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Fine +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fine +material = generic_nylon_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 10 +material_print_temperature = =default_material_print_temperature + 20 +build_volume_temperature = 40 +speed_print = 30 +retraction_amount = 1 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PA_normal.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PA_normal.inst.cfg new file mode 100644 index 0000000000..f1ea09cee7 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PA_normal.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Normal +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +material = generic_nylon_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 10 +material_print_temperature = =default_material_print_temperature + 20 +build_volume_temperature = 40 +speed_print = 30 +retraction_amount = 1 +cool_fan_enabled = False diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PC_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PC_extrafast.inst.cfg new file mode 100644 index 0000000000..5a0d430cd5 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PC_extrafast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_pc_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 13 +build_volume_temperature = 45 +speed_print = 25 +retraction_amount = 1 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PC_fast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PC_fast.inst.cfg new file mode 100644 index 0000000000..8001b6c527 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PC_fast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fast +material = generic_pc_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 13 +build_volume_temperature = 45 +speed_print = 25 +retraction_amount = 1 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PC_fine.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PC_fine.inst.cfg new file mode 100644 index 0000000000..35d424435b --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PC_fine.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Fine +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fine +material = generic_pc_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 13 +build_volume_temperature = 45 +speed_print = 25 +retraction_amount = 1 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PC_normal.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PC_normal.inst.cfg new file mode 100644 index 0000000000..52d769949d --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PC_normal.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Normal +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +material = generic_pc_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 13 +build_volume_temperature = 45 +speed_print = 25 +retraction_amount = 1 +cool_fan_enabled = False diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PETG_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PETG_extrafast.inst.cfg new file mode 100644 index 0000000000..2fc6811bb4 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PETG_extrafast.inst.cfg @@ -0,0 +1,21 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_petg_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature - 5 +material_print_temperature = =default_material_print_temperature + 30 +speed_print = 60 +retraction_amount = 1 +cool_fan_speed = 30 +cool_fan_speed_max = 50 +support_fan_enable = True +support_supported_skin_fan_speed = 100 diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PETG_fast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PETG_fast.inst.cfg new file mode 100644 index 0000000000..3f9524b676 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PETG_fast.inst.cfg @@ -0,0 +1,21 @@ +[general] +version = 4 +name = Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fast +material = generic_petg_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature - 5 +material_print_temperature = =default_material_print_temperature + 30 +speed_print = 60 +retraction_amount = 1 +cool_fan_speed = 30 +cool_fan_speed_max = 50 +support_fan_enable = True +support_supported_skin_fan_speed = 100 diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PETG_fine.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PETG_fine.inst.cfg new file mode 100644 index 0000000000..e4acd17ee3 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PETG_fine.inst.cfg @@ -0,0 +1,21 @@ +[general] +version = 4 +name = Fine +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fine +material = generic_petg_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature - 5 +material_print_temperature = =default_material_print_temperature + 30 +speed_print = 60 +retraction_amount = 1 +cool_fan_speed = 30 +cool_fan_speed_max = 50 +support_fan_enable = True +support_supported_skin_fan_speed = 100 diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PETG_normal.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PETG_normal.inst.cfg new file mode 100644 index 0000000000..d1dca211c3 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PETG_normal.inst.cfg @@ -0,0 +1,21 @@ +[general] +version = 4 +name = Normal +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +material = generic_petg_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature - 5 +material_print_temperature = =default_material_print_temperature + 30 +speed_print = 60 +retraction_amount = 1 +cool_fan_speed = 30 +cool_fan_speed_max = 50 +support_fan_enable = True +support_supported_skin_fan_speed = 100 diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PLA_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PLA_extrafast.inst.cfg new file mode 100644 index 0000000000..5c08520659 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PLA_extrafast.inst.cfg @@ -0,0 +1,20 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_pla_175 +variant = V6 0.40mm + +[values] +material_print_temperature = =default_material_print_temperature + 20 +speed_print = 70 +retraction_amount = 0.75 +cool_fan_speed = 75 +cool_fan_speed_max = 100 +support_fan_enable = True +support_supported_skin_fan_speed = 100 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PLA_fast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PLA_fast.inst.cfg new file mode 100644 index 0000000000..63e31f6b85 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PLA_fast.inst.cfg @@ -0,0 +1,20 @@ +[general] +version = 4 +name = Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fast +material = generic_pla_175 +variant = V6 0.40mm + +[values] +material_print_temperature = =default_material_print_temperature + 15 +speed_print = 70 +retraction_amount = 0.75 +cool_fan_speed = 75 +cool_fan_speed_max = 100 +support_fan_enable = True +support_supported_skin_fan_speed = 100 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PLA_fine.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PLA_fine.inst.cfg new file mode 100644 index 0000000000..857a0541ff --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PLA_fine.inst.cfg @@ -0,0 +1,20 @@ +[general] +version = 4 +name = Fine +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fine +material = generic_pla_175 +variant = V6 0.40mm + +[values] +material_print_temperature = =default_material_print_temperature + 5 +speed_print = 70 +retraction_amount = 0.75 +cool_fan_speed = 75 +cool_fan_speed_max = 100 +support_fan_enable = True +support_supported_skin_fan_speed = 100 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PLA_normal.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PLA_normal.inst.cfg new file mode 100644 index 0000000000..8e030de41e --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PLA_normal.inst.cfg @@ -0,0 +1,20 @@ +[general] +version = 4 +name = Normal +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +material = generic_pla_175 +variant = V6 0.40mm + +[values] +material_print_temperature = =default_material_print_temperature + 10 +speed_print = 70 +retraction_amount = 0.75 +cool_fan_speed = 75 +cool_fan_speed_max = 100 +support_fan_enable = True +support_supported_skin_fan_speed = 100 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PVA_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PVA_extrafast.inst.cfg new file mode 100644 index 0000000000..471c9fa585 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PVA_extrafast.inst.cfg @@ -0,0 +1,24 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_pva_175 +variant = V6 0.40mm + +[values] +material_print_temperature = =default_material_print_temperature +speed_print = 30 +retraction_amount = 1 +cool_fan_enabled = False +brim_replaces_support = False +support_brim_enable = True +support_pattern = triangles +support_xy_distance_overhang = 0 +support_bottom_distance = =support_z_distance +support_use_towers = False +support_z_distance = 0 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PVA_fast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PVA_fast.inst.cfg new file mode 100644 index 0000000000..2ff44b84a7 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PVA_fast.inst.cfg @@ -0,0 +1,24 @@ +[general] +version = 4 +name = Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fast +material = generic_pva_175 +variant = V6 0.40mm + +[values] +material_print_temperature = =default_material_print_temperature +speed_print = 30 +retraction_amount = 1 +cool_fan_enabled = False +brim_replaces_support = False +support_brim_enable = True +support_pattern = triangles +support_xy_distance_overhang = 0 +support_bottom_distance = =support_z_distance +support_use_towers = False +support_z_distance = 0 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PVA_fine.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PVA_fine.inst.cfg new file mode 100644 index 0000000000..3c42037ac2 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PVA_fine.inst.cfg @@ -0,0 +1,24 @@ +[general] +version = 4 +name = Fine +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fine +material = generic_pva_175 +variant = V6 0.40mm + +[values] +material_print_temperature = =default_material_print_temperature +speed_print = 30 +retraction_amount = 1 +cool_fan_enabled = False +brim_replaces_support = False +support_brim_enable = True +support_pattern = triangles +support_xy_distance_overhang = 0 +support_bottom_distance = =support_z_distance +support_use_towers = False +support_z_distance = 0 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PVA_normal.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PVA_normal.inst.cfg new file mode 100644 index 0000000000..cb637e253f --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_PVA_normal.inst.cfg @@ -0,0 +1,24 @@ +[general] +version = 4 +name = Normal +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +material = generic_pva_175 +variant = V6 0.40mm + +[values] +material_print_temperature = =default_material_print_temperature +speed_print = 30 +retraction_amount = 1 +cool_fan_enabled = False +brim_replaces_support = False +support_brim_enable = True +support_pattern = triangles +support_xy_distance_overhang = 0 +support_bottom_distance = =support_z_distance +support_use_towers = False +support_z_distance = 0 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_TPU_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_TPU_extrafast.inst.cfg new file mode 100644 index 0000000000..cc2f9861fb --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_TPU_extrafast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_tpu_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 60 +material_print_temperature = =default_material_print_temperature + 20 +build_volume_temperature = 30 +speed_print = 30 +retraction_amount = 1 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_TPU_fast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_TPU_fast.inst.cfg new file mode 100644 index 0000000000..bb4bb3aa4c --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_TPU_fast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fast +material = generic_tpu_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 60 +material_print_temperature = =default_material_print_temperature + 20 +build_volume_temperature = 30 +speed_print = 30 +retraction_amount = 1 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_TPU_fine.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_TPU_fine.inst.cfg new file mode 100644 index 0000000000..77dae49a9f --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_TPU_fine.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Fine +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = fine +material = generic_tpu_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 60 +material_print_temperature = =default_material_print_temperature + 20 +build_volume_temperature = 30 +speed_print = 30 +retraction_amount = 1 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_TPU_normal.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_TPU_normal.inst.cfg new file mode 100644 index 0000000000..e51ce57400 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.40_TPU_normal.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Normal +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +material = generic_tpu_175 +variant = V6 0.40mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 60 +material_print_temperature = =default_material_print_temperature + 20 +build_volume_temperature = 30 +speed_print = 30 +retraction_amount = 1 +cool_fan_enabled = False diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_ABS_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_ABS_extrafast.inst.cfg new file mode 100644 index 0000000000..677117e5ac --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_ABS_extrafast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_abs_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 25 +build_volume_temperature = 40 +speed_print = 50 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_ABS_sprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_ABS_sprint.inst.cfg new file mode 100644 index 0000000000..dd19a525b9 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_ABS_sprint.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = sprint +material = generic_abs_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 25 +build_volume_temperature = 40 +speed_print = 50 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_ABS_supersprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_ABS_supersprint.inst.cfg new file mode 100644 index 0000000000..f47ee424d1 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_ABS_supersprint.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Super Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = supersprint +material = generic_abs_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 25 +build_volume_temperature = 40 +speed_print = 50 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_HIPS_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_HIPS_extrafast.inst.cfg new file mode 100644 index 0000000000..6a429d7e74 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_HIPS_extrafast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_hips_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 35 +material_print_temperature = =default_material_print_temperature + 50 +build_volume_temperature = 40 +speed_print = 40 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_HIPS_sprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_HIPS_sprint.inst.cfg new file mode 100644 index 0000000000..8176b0003e --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_HIPS_sprint.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = sprint +material = generic_hips_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 35 +material_print_temperature = =default_material_print_temperature + 50 +build_volume_temperature = 40 +speed_print = 40 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_HIPS_supersprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_HIPS_supersprint.inst.cfg new file mode 100644 index 0000000000..403d99f4ad --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_HIPS_supersprint.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Super Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = supersprint +material = generic_hips_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 35 +material_print_temperature = =default_material_print_temperature + 50 +build_volume_temperature = 40 +speed_print = 40 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PA_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PA_extrafast.inst.cfg new file mode 100644 index 0000000000..5e3330de0d --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PA_extrafast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_nylon_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 20 +build_volume_temperature = 40 +speed_print = 30 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PA_sprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PA_sprint.inst.cfg new file mode 100644 index 0000000000..f3223a9895 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PA_sprint.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = sprint +material = generic_nylon_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 20 +build_volume_temperature = 40 +speed_print = 30 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PA_supersprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PA_supersprint.inst.cfg new file mode 100644 index 0000000000..df3e575b6e --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PA_supersprint.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Super Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = supersprint +material = generic_nylon_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 20 +build_volume_temperature = 40 +speed_print = 30 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PC_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PC_extrafast.inst.cfg new file mode 100644 index 0000000000..04498b7b4a --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PC_extrafast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_pc_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 13 +build_volume_temperature = 45 +speed_print = 25 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PC_sprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PC_sprint.inst.cfg new file mode 100644 index 0000000000..94644ce36a --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PC_sprint.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = sprint +material = generic_pc_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 13 +build_volume_temperature = 45 +speed_print = 25 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PC_supersprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PC_supersprint.inst.cfg new file mode 100644 index 0000000000..3f72476b1d --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PC_supersprint.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Super Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = supersprint +material = generic_pc_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 20 +material_print_temperature = =default_material_print_temperature + 13 +build_volume_temperature = 45 +speed_print = 25 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PETG_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PETG_extrafast.inst.cfg new file mode 100644 index 0000000000..c76b2953aa --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PETG_extrafast.inst.cfg @@ -0,0 +1,21 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_petg_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature - 5 +material_print_temperature = =default_material_print_temperature + 40 +speed_print = 40 +retraction_amount = 1.2 +cool_fan_speed = 30 +cool_fan_speed_max = 50 +support_fan_enable = True +support_supported_skin_fan_speed = 100 diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PETG_sprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PETG_sprint.inst.cfg new file mode 100644 index 0000000000..913bf5044b --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PETG_sprint.inst.cfg @@ -0,0 +1,21 @@ +[general] +version = 4 +name = Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = sprint +material = generic_petg_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature - 5 +material_print_temperature = =default_material_print_temperature + 40 +speed_print = 30 +retraction_amount = 1.2 +cool_fan_speed = 30 +cool_fan_speed_max = 50 +support_fan_enable = True +support_supported_skin_fan_speed = 100 diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PETG_supersprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PETG_supersprint.inst.cfg new file mode 100644 index 0000000000..1aea879ad6 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PETG_supersprint.inst.cfg @@ -0,0 +1,21 @@ +[general] +version = 4 +name = Super Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = supersprint +material = generic_petg_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature - 5 +material_print_temperature = =default_material_print_temperature + 40 +speed_print = 24 +retraction_amount = 1.2 +cool_fan_speed = 30 +cool_fan_speed_max = 50 +support_fan_enable = True +support_supported_skin_fan_speed = 100 diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PLA_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PLA_extrafast.inst.cfg new file mode 100644 index 0000000000..fa2f6773a5 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PLA_extrafast.inst.cfg @@ -0,0 +1,20 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_pla_175 +variant = V6 0.80mm + +[values] +material_print_temperature = =default_material_print_temperature + 25 +speed_print = 40 +retraction_amount = 1 +cool_fan_speed = 75 +cool_fan_speed_max = 100 +support_fan_enable = True +support_supported_skin_fan_speed = 100 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PLA_sprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PLA_sprint.inst.cfg new file mode 100644 index 0000000000..c8a59df79f --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PLA_sprint.inst.cfg @@ -0,0 +1,20 @@ +[general] +version = 4 +name = Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = sprint +material = generic_pla_175 +variant = V6 0.80mm + +[values] +material_print_temperature = =default_material_print_temperature + 30 +speed_print = 30 +retraction_amount = 1 +cool_fan_speed = 75 +cool_fan_speed_max = 100 +support_fan_enable = True +support_supported_skin_fan_speed = 100 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PLA_supersprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PLA_supersprint.inst.cfg new file mode 100644 index 0000000000..8ed704f87e --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PLA_supersprint.inst.cfg @@ -0,0 +1,20 @@ +[general] +version = 4 +name = Super Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = supersprint +material = generic_pla_175 +variant = V6 0.80mm + +[values] +material_print_temperature = =default_material_print_temperature + 30 +speed_print = 24 +retraction_amount = 1 +cool_fan_speed = 75 +cool_fan_speed_max = 100 +support_fan_enable = True +support_supported_skin_fan_speed = 100 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PVA_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PVA_extrafast.inst.cfg new file mode 100644 index 0000000000..a2a691564e --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PVA_extrafast.inst.cfg @@ -0,0 +1,24 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_pva_175 +variant = V6 0.80mm + +[values] +material_print_temperature = =default_material_print_temperature +speed_print = 30 +retraction_amount = 1.2 +cool_fan_enabled = False +brim_replaces_support = False +support_brim_enable = True +support_pattern = triangles +support_xy_distance_overhang = 0 +support_bottom_distance = =support_z_distance +support_use_towers = False +support_z_distance = 0 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PVA_sprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PVA_sprint.inst.cfg new file mode 100644 index 0000000000..3e408d62df --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PVA_sprint.inst.cfg @@ -0,0 +1,24 @@ +[general] +version = 4 +name = Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = sprint +material = generic_pva_175 +variant = V6 0.80mm + +[values] +material_print_temperature = =default_material_print_temperature +speed_print = 30 +retraction_amount = 1.2 +cool_fan_enabled = False +brim_replaces_support = False +support_brim_enable = True +support_pattern = triangles +support_xy_distance_overhang = 0 +support_bottom_distance = =support_z_distance +support_use_towers = False +support_z_distance = 0 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PVA_supersprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PVA_supersprint.inst.cfg new file mode 100644 index 0000000000..00e67d1889 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_PVA_supersprint.inst.cfg @@ -0,0 +1,24 @@ +[general] +version = 4 +name = Super Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = supersprint +material = generic_pva_175 +variant = V6 0.80mm + +[values] +material_print_temperature = =default_material_print_temperature +speed_print = 30 +retraction_amount = 1.2 +cool_fan_enabled = False +brim_replaces_support = False +support_brim_enable = True +support_pattern = triangles +support_xy_distance_overhang = 0 +support_bottom_distance = =support_z_distance +support_use_towers = False +support_z_distance = 0 \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_TPU_extrafast.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_TPU_extrafast.inst.cfg new file mode 100644 index 0000000000..462396f6a0 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_TPU_extrafast.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Extra Fast +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = extrafast +material = generic_tpu_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 60 +material_print_temperature = =default_material_print_temperature + 25 +build_volume_temperature = 30 +speed_print = 30 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_TPU_sprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_TPU_sprint.inst.cfg new file mode 100644 index 0000000000..a4467ed8d7 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_TPU_sprint.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = sprint +material = generic_tpu_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 60 +material_print_temperature = =default_material_print_temperature + 25 +build_volume_temperature = 30 +speed_print = 30 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_TPU_supersprint.inst.cfg b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_TPU_supersprint.inst.cfg new file mode 100644 index 0000000000..4ca135b408 --- /dev/null +++ b/resources/quality/atmat_signal_pro/signal_pro_v6_0.80_TPU_supersprint.inst.cfg @@ -0,0 +1,19 @@ +[general] +version = 4 +name = Super Sprint +definition = atmat_signal_pro_base + +[metadata] +setting_version = 15 +type = quality +quality_type = supersprint +material = generic_tpu_175 +variant = V6 0.80mm + +[values] +material_bed_temperature = =default_material_bed_temperature + 60 +material_print_temperature = =default_material_print_temperature + 25 +build_volume_temperature = 30 +speed_print = 30 +retraction_amount = 1.2 +cool_fan_enabled = False \ No newline at end of file diff --git a/resources/quality/dagoma/dagoma_discoeasy200_bicolor_pla_fast.inst.cfg b/resources/quality/dagoma/dagoma_discoeasy200_bicolor_pla_fast.inst.cfg new file mode 100644 index 0000000000..8e126094cc --- /dev/null +++ b/resources/quality/dagoma/dagoma_discoeasy200_bicolor_pla_fast.inst.cfg @@ -0,0 +1,26 @@ +[general] +version = 4 +name = Fast +definition = dagoma_discoeasy200_bicolor + +[metadata] +setting_version = 15 +type = quality +quality_type = draft +weight = -2 +material = chromatik_pla + +[values] +layer_height = 0.2 +line_width = =machine_nozzle_size * 0.875 + +material_print_temperature = =default_material_print_temperature + 10 +material_bed_temperature_layer_0 = =default_material_bed_temperature + 10 + +speed_print = 60 +speed_travel = 75 +speed_layer_0 = 17 +speed_infill = 60 +speed_wall_0 = 50 +speed_wall_x = 60 +speed_topbottom = 60 diff --git a/resources/quality/dagoma/dagoma_discoeasy200_bicolor_pla_fine.inst.cfg b/resources/quality/dagoma/dagoma_discoeasy200_bicolor_pla_fine.inst.cfg new file mode 100644 index 0000000000..b26282ecb7 --- /dev/null +++ b/resources/quality/dagoma/dagoma_discoeasy200_bicolor_pla_fine.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 4 +name = Fine +definition = dagoma_discoeasy200_bicolor + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +weight = 0 +material = chromatik_pla + +[values] +layer_height = 0.1 +line_width = =machine_nozzle_size * 0.875 + +speed_print = 35 +speed_travel = 50 +speed_layer_0 = 15 +speed_infill = 40 +speed_wall_0 = 25 +speed_wall_x = 35 +speed_topbottom = 35 diff --git a/resources/quality/dagoma/dagoma_discoeasy200_bicolor_pla_standard.inst.cfg b/resources/quality/dagoma/dagoma_discoeasy200_bicolor_pla_standard.inst.cfg new file mode 100644 index 0000000000..6bc4310ce4 --- /dev/null +++ b/resources/quality/dagoma/dagoma_discoeasy200_bicolor_pla_standard.inst.cfg @@ -0,0 +1,26 @@ +[general] +version = 4 +name = Standard +definition = dagoma_discoeasy200_bicolor + +[metadata] +setting_version = 15 +type = quality +quality_type = fast +weight = -1 +material = chromatik_pla + +[values] +layer_height = 0.15 +line_width = =machine_nozzle_size * 0.875 + +material_print_temperature = =default_material_print_temperature + 5 +material_bed_temperature_layer_0 = =default_material_bed_temperature + 5 + +speed_print = 50 +speed_travel = 60 +speed_layer_0 = 17 +speed_infill = 50 +speed_wall_0 = 40 +speed_wall_x = 45 +speed_topbottom = 50 diff --git a/resources/quality/dagoma/dagoma_discoultimate_bicolor_pla_fast.inst.cfg b/resources/quality/dagoma/dagoma_discoultimate_bicolor_pla_fast.inst.cfg new file mode 100644 index 0000000000..c08bd89d1e --- /dev/null +++ b/resources/quality/dagoma/dagoma_discoultimate_bicolor_pla_fast.inst.cfg @@ -0,0 +1,26 @@ +[general] +version = 4 +name = Fast +definition = dagoma_discoultimate_bicolor + +[metadata] +setting_version = 15 +type = quality +quality_type = draft +weight = -2 +material = chromatik_pla + +[values] +layer_height = 0.2 +line_width = =machine_nozzle_size * 0.875 + +material_print_temperature = =default_material_print_temperature + 10 +material_bed_temperature_layer_0 = =default_material_bed_temperature + 10 + +speed_print = 60 +speed_travel = 75 +speed_layer_0 = 17 +speed_infill = 60 +speed_wall_0 = 50 +speed_wall_x = 60 +speed_topbottom = 60 diff --git a/resources/quality/dagoma/dagoma_discoultimate_bicolor_pla_fine.inst.cfg b/resources/quality/dagoma/dagoma_discoultimate_bicolor_pla_fine.inst.cfg new file mode 100644 index 0000000000..7795d6aaa9 --- /dev/null +++ b/resources/quality/dagoma/dagoma_discoultimate_bicolor_pla_fine.inst.cfg @@ -0,0 +1,23 @@ +[general] +version = 4 +name = Fine +definition = dagoma_discoultimate_bicolor + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +weight = 0 +material = chromatik_pla + +[values] +layer_height = 0.1 +line_width = =machine_nozzle_size * 0.875 + +speed_print = 35 +speed_travel = 50 +speed_layer_0 = 15 +speed_infill = 40 +speed_wall_0 = 25 +speed_wall_x = 35 +speed_topbottom = 35 diff --git a/resources/quality/dagoma/dagoma_discoultimate_bicolor_pla_standard.inst.cfg b/resources/quality/dagoma/dagoma_discoultimate_bicolor_pla_standard.inst.cfg new file mode 100644 index 0000000000..7015261d3b --- /dev/null +++ b/resources/quality/dagoma/dagoma_discoultimate_bicolor_pla_standard.inst.cfg @@ -0,0 +1,26 @@ +[general] +version = 4 +name = Standard +definition = dagoma_discoultimate_bicolor + +[metadata] +setting_version = 15 +type = quality +quality_type = fast +weight = -1 +material = chromatik_pla + +[values] +layer_height = 0.15 +line_width = =machine_nozzle_size * 0.875 + +material_print_temperature = =default_material_print_temperature + 5 +material_bed_temperature_layer_0 = =default_material_bed_temperature + 5 + +speed_print = 50 +speed_travel = 60 +speed_layer_0 = 17 +speed_infill = 50 +speed_wall_0 = 40 +speed_wall_x = 45 +speed_topbottom = 50 diff --git a/resources/quality/tinyboy/tinyboy_e10_draft.inst.cfg b/resources/quality/tinyboy/tinyboy_e10_draft.inst.cfg new file mode 100644 index 0000000000..2f8b5f41b0 --- /dev/null +++ b/resources/quality/tinyboy/tinyboy_e10_draft.inst.cfg @@ -0,0 +1,61 @@ +[general] +version = 4 +name = Draft +definition = tinyboy_e10 + +[metadata] +setting_version = 15 +type = quality +quality_type = draft +weight = 0 +global_quality = True + +[values] +acceleration_enabled = True +acceleration_print = 1800 +acceleration_travel = 3000 +adhesion_type = skirt +brim_width = 4.0 +cool_fan_full_at_height = 0.5 +cool_fan_speed = 100 +cool_fan_speed_0 = 100 +infill_overlap = 15 +infill_pattern = zigzag +infill_sparse_density = 25 +initial_layer_line_width_factor = 140 +jerk_enabled = True +jerk_print = 8 +jerk_travel = 10 +layer_height = 0.3 +layer_height_0 = 0.3 +material_bed_temperature = 60 +material_diameter = 1.75 +material_print_temperature = 200 +material_print_temperature_layer_0 = 0 +retract_at_layer_change = False +retraction_amount = 6 +retraction_hop = 0.075 +retraction_hop_enabled = True +retraction_hop_only_when_collides = True +retraction_min_travel = 1.5 +retraction_speed = 40 +skirt_brim_speed = 40 +skirt_gap = 5 +skirt_line_count = 3 +speed_infill = =speed_print +speed_print = 60 +speed_support = 60 +speed_topbottom = =math.ceil(speed_print * 30 / 60) +speed_travel = 100 +speed_wall = =speed_print +speed_wall_x = =speed_print +support_angle = 60 +support_enable = True +support_interface_enable = True +support_pattern = triangles +support_roof_enable = True +support_type = everywhere +support_use_towers = False +support_xy_distance = 0.7 +top_bottom_thickness = 1.2 +wall_thickness = 1.2 diff --git a/resources/quality/tinyboy/tinyboy_e10_high.inst.cfg b/resources/quality/tinyboy/tinyboy_e10_high.inst.cfg new file mode 100644 index 0000000000..75ec4a5334 --- /dev/null +++ b/resources/quality/tinyboy/tinyboy_e10_high.inst.cfg @@ -0,0 +1,61 @@ +[general] +version = 4 +name = High +definition = tinyboy_e10 + +[metadata] +setting_version = 15 +type = quality +quality_type = high +weight = 2 +global_quality = True + +[values] +acceleration_enabled = True +acceleration_print = 1800 +acceleration_travel = 3000 +adhesion_type = skirt +brim_width = 4.0 +cool_fan_full_at_height = 0.5 +cool_fan_speed = 100 +cool_fan_speed_0 = 100 +infill_overlap = 15 +infill_pattern = zigzag +infill_sparse_density = 25 +initial_layer_line_width_factor = 140 +jerk_enabled = True +jerk_print = 8 +jerk_travel = 10 +layer_height = 0.1 +layer_height_0 = 0.1 +material_bed_temperature = 60 +material_diameter = 1.75 +material_print_temperature = 200 +material_print_temperature_layer_0 = 0 +retract_at_layer_change = False +retraction_amount = 6 +retraction_hop = 0.075 +retraction_hop_enabled = True +retraction_hop_only_when_collides = True +retraction_min_travel = 1.5 +retraction_speed = 40 +skirt_brim_speed = 40 +skirt_gap = 5 +skirt_line_count = 3 +speed_infill = =speed_print +speed_print = 50 +speed_support = 30 +speed_topbottom = =math.ceil(speed_print * 20 / 50) +speed_travel = 50 +speed_wall = =speed_print +speed_wall_x = =speed_print +support_angle = 60 +support_enable = True +support_interface_enable = True +support_pattern = triangles +support_roof_enable = True +support_type = everywhere +support_use_towers = False +support_xy_distance = 0.7 +top_bottom_thickness = 1.2 +wall_thickness = 1.2 diff --git a/resources/quality/tinyboy/tinyboy_e10_normal.inst.cfg b/resources/quality/tinyboy/tinyboy_e10_normal.inst.cfg new file mode 100644 index 0000000000..983bd24281 --- /dev/null +++ b/resources/quality/tinyboy/tinyboy_e10_normal.inst.cfg @@ -0,0 +1,61 @@ +[general] +version = 4 +name = Normal +definition = tinyboy_e10 + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +weight = 1 +global_quality = True + +[values] +acceleration_enabled = True +acceleration_print = 1800 +acceleration_travel = 3000 +adhesion_type = skirt +brim_width = 4.0 +cool_fan_full_at_height = 0.5 +cool_fan_speed = 100 +cool_fan_speed_0 = 100 +infill_overlap = 15 +infill_pattern = zigzag +infill_sparse_density = 25 +initial_layer_line_width_factor = 140 +jerk_enabled = True +jerk_print = 8 +jerk_travel = 10 +layer_height = 0.2 +layer_height_0 = 0.2 +material_bed_temperature = 60 +material_diameter = 1.75 +material_print_temperature = 200 +material_print_temperature_layer_0 = 0 +retract_at_layer_change = False +retraction_amount = 6 +retraction_hop = 0.075 +retraction_hop_enabled = True +retraction_hop_only_when_collides = True +retraction_min_travel = 1.5 +retraction_speed = 40 +skirt_brim_speed = 40 +skirt_gap = 5 +skirt_line_count = 3 +speed_infill = =speed_print +speed_print = 50 +speed_support = 30 +speed_topbottom = =math.ceil(speed_print * 20 / 50) +speed_travel = 100 +speed_wall = =speed_print +speed_wall_x = =speed_print +support_angle = 60 +support_enable = True +support_interface_enable = True +support_pattern = triangles +support_roof_enable = True +support_type = everywhere +support_use_towers = False +support_xy_distance = 0.7 +top_bottom_thickness = 1.2 +wall_thickness = 1.2 diff --git a/resources/quality/tinyboy/tinyboy_e16_draft.inst.cfg b/resources/quality/tinyboy/tinyboy_e16_draft.inst.cfg new file mode 100644 index 0000000000..7c3ef67dad --- /dev/null +++ b/resources/quality/tinyboy/tinyboy_e16_draft.inst.cfg @@ -0,0 +1,61 @@ +[general] +version = 4 +name = Draft +definition = tinyboy_e16 + +[metadata] +setting_version = 15 +type = quality +quality_type = draft +weight = 0 +global_quality = True + +[values] +acceleration_enabled = True +acceleration_print = 1800 +acceleration_travel = 3000 +adhesion_type = skirt +brim_width = 4.0 +cool_fan_full_at_height = 0.5 +cool_fan_speed = 100 +cool_fan_speed_0 = 100 +infill_overlap = 15 +infill_pattern = zigzag +infill_sparse_density = 25 +initial_layer_line_width_factor = 140 +jerk_enabled = True +jerk_print = 8 +jerk_travel = 10 +layer_height = 0.3 +layer_height_0 = 0.3 +material_bed_temperature = 60 +material_diameter = 1.75 +material_print_temperature = 200 +material_print_temperature_layer_0 = 0 +retract_at_layer_change = False +retraction_amount = 6 +retraction_hop = 0.075 +retraction_hop_enabled = True +retraction_hop_only_when_collides = True +retraction_min_travel = 1.5 +retraction_speed = 40 +skirt_brim_speed = 40 +skirt_gap = 5 +skirt_line_count = 3 +speed_infill = =speed_print +speed_print = 60 +speed_support = 60 +speed_topbottom = =math.ceil(speed_print * 30 / 60) +speed_travel = 100 +speed_wall = =speed_print +speed_wall_x = =speed_print +support_angle = 60 +support_enable = True +support_interface_enable = True +support_pattern = triangles +support_roof_enable = True +support_type = everywhere +support_use_towers = False +support_xy_distance = 0.7 +top_bottom_thickness = 1.2 +wall_thickness = 1.2 diff --git a/resources/quality/tinyboy/tinyboy_e16_high.inst.cfg b/resources/quality/tinyboy/tinyboy_e16_high.inst.cfg new file mode 100644 index 0000000000..7a2be11d61 --- /dev/null +++ b/resources/quality/tinyboy/tinyboy_e16_high.inst.cfg @@ -0,0 +1,61 @@ +[general] +version = 4 +name = High +definition = tinyboy_e16 + +[metadata] +setting_version = 15 +type = quality +quality_type = high +weight = 2 +global_quality = True + +[values] +acceleration_enabled = True +acceleration_print = 1800 +acceleration_travel = 3000 +adhesion_type = skirt +brim_width = 4.0 +cool_fan_full_at_height = 0.5 +cool_fan_speed = 100 +cool_fan_speed_0 = 100 +infill_overlap = 15 +infill_pattern = zigzag +infill_sparse_density = 25 +initial_layer_line_width_factor = 140 +jerk_enabled = True +jerk_print = 8 +jerk_travel = 10 +layer_height = 0.1 +layer_height_0 = 0.1 +material_bed_temperature = 60 +material_diameter = 1.75 +material_print_temperature = 200 +material_print_temperature_layer_0 = 0 +retract_at_layer_change = False +retraction_amount = 6 +retraction_hop = 0.075 +retraction_hop_enabled = True +retraction_hop_only_when_collides = True +retraction_min_travel = 1.5 +retraction_speed = 40 +skirt_brim_speed = 40 +skirt_gap = 5 +skirt_line_count = 3 +speed_infill = =speed_print +speed_print = 50 +speed_support = 30 +speed_topbottom = =math.ceil(speed_print * 20 / 50) +speed_travel = 50 +speed_wall = =speed_print +speed_wall_x = =speed_print +support_angle = 60 +support_enable = True +support_interface_enable = True +support_pattern = triangles +support_roof_enable = True +support_type = everywhere +support_use_towers = False +support_xy_distance = 0.7 +top_bottom_thickness = 1.2 +wall_thickness = 1.2 diff --git a/resources/quality/tinyboy/tinyboy_e16_normal.inst.cfg b/resources/quality/tinyboy/tinyboy_e16_normal.inst.cfg new file mode 100644 index 0000000000..598dc15d01 --- /dev/null +++ b/resources/quality/tinyboy/tinyboy_e16_normal.inst.cfg @@ -0,0 +1,61 @@ +[general] +version = 4 +name = Normal +definition = tinyboy_e16 + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +weight = 1 +global_quality = True + +[values] +acceleration_enabled = True +acceleration_print = 1800 +acceleration_travel = 3000 +adhesion_type = skirt +brim_width = 4.0 +cool_fan_full_at_height = 0.5 +cool_fan_speed = 100 +cool_fan_speed_0 = 100 +infill_overlap = 15 +infill_pattern = zigzag +infill_sparse_density = 25 +initial_layer_line_width_factor = 140 +jerk_enabled = True +jerk_print = 8 +jerk_travel = 10 +layer_height = 0.2 +layer_height_0 = 0.2 +material_bed_temperature = 60 +material_diameter = 1.75 +material_print_temperature = 200 +material_print_temperature_layer_0 = 0 +retract_at_layer_change = False +retraction_amount = 6 +retraction_hop = 0.075 +retraction_hop_enabled = True +retraction_hop_only_when_collides = True +retraction_min_travel = 1.5 +retraction_speed = 40 +skirt_brim_speed = 40 +skirt_gap = 5 +skirt_line_count = 3 +speed_infill = =speed_print +speed_print = 50 +speed_support = 30 +speed_topbottom = =math.ceil(speed_print * 20 / 50) +speed_travel = 100 +speed_wall = =speed_print +speed_wall_x = =speed_print +support_angle = 60 +support_enable = True +support_interface_enable = True +support_pattern = triangles +support_roof_enable = True +support_type = everywhere +support_use_towers = False +support_xy_distance = 0.7 +top_bottom_thickness = 1.2 +wall_thickness = 1.2 diff --git a/resources/quality/tinyboy/tinyboy_ra20_draft.inst.cfg b/resources/quality/tinyboy/tinyboy_ra20_draft.inst.cfg new file mode 100644 index 0000000000..93cf00c007 --- /dev/null +++ b/resources/quality/tinyboy/tinyboy_ra20_draft.inst.cfg @@ -0,0 +1,61 @@ +[general] +version = 4 +name = Draft +definition = tinyboy_ra20 + +[metadata] +setting_version = 15 +type = quality +quality_type = draft +weight = 0 +global_quality = True + +[values] +acceleration_enabled = True +acceleration_print = 1800 +acceleration_travel = 3000 +adhesion_type = none +brim_width = 4.0 +cool_fan_full_at_height = 0.5 +cool_fan_speed = 100 +cool_fan_speed_0 = 100 +infill_overlap = 15 +infill_pattern = zigzag +infill_sparse_density = 25 +initial_layer_line_width_factor = 140 +jerk_enabled = True +jerk_print = 8 +jerk_travel = 10 +layer_height = 0.3 +layer_height_0 = 0.3 +material_bed_temperature = 60 +material_diameter = 1.75 +material_print_temperature = 200 +material_print_temperature_layer_0 = 0 +retract_at_layer_change = False +retraction_amount = 6 +retraction_hop = 0.075 +retraction_hop_enabled = True +retraction_hop_only_when_collides = True +retraction_min_travel = 1.5 +retraction_speed = 40 +skirt_brim_speed = 40 +skirt_gap = 5 +skirt_line_count = 3 +speed_infill = =speed_print +speed_print = 60 +speed_support = 60 +speed_topbottom = =math.ceil(speed_print * 30 / 60) +speed_travel = 100 +speed_wall = =speed_print +speed_wall_x = =speed_print +support_angle = 60 +support_enable = True +support_interface_enable = True +support_pattern = triangles +support_roof_enable = True +support_type = everywhere +support_use_towers = False +support_xy_distance = 0.7 +top_bottom_thickness = 1.2 +wall_thickness = 1.2 diff --git a/resources/quality/tinyboy/tinyboy_ra20_high.inst.cfg b/resources/quality/tinyboy/tinyboy_ra20_high.inst.cfg new file mode 100644 index 0000000000..8d02c663b6 --- /dev/null +++ b/resources/quality/tinyboy/tinyboy_ra20_high.inst.cfg @@ -0,0 +1,61 @@ +[general] +version = 4 +name = High +definition = tinyboy_ra20 + +[metadata] +setting_version = 15 +type = quality +quality_type = high +weight = 2 +global_quality = True + +[values] +acceleration_enabled = True +acceleration_print = 1800 +acceleration_travel = 3000 +adhesion_type = none +brim_width = 4.0 +cool_fan_full_at_height = 0.5 +cool_fan_speed = 100 +cool_fan_speed_0 = 100 +infill_overlap = 15 +infill_pattern = zigzag +infill_sparse_density = 25 +initial_layer_line_width_factor = 140 +jerk_enabled = True +jerk_print = 8 +jerk_travel = 10 +layer_height = 0.1 +layer_height_0 = 0.1 +material_bed_temperature = 60 +material_diameter = 1.75 +material_print_temperature = 200 +material_print_temperature_layer_0 = 0 +retract_at_layer_change = False +retraction_amount = 6 +retraction_hop = 0.075 +retraction_hop_enabled = True +retraction_hop_only_when_collides = True +retraction_min_travel = 1.5 +retraction_speed = 40 +skirt_brim_speed = 40 +skirt_gap = 5 +skirt_line_count = 3 +speed_infill = =speed_print +speed_print = 50 +speed_support = 30 +speed_topbottom = =math.ceil(speed_print * 20 / 50) +speed_travel = 50 +speed_wall = =speed_print +speed_wall_x = =speed_print +support_angle = 60 +support_enable = True +support_interface_enable = True +support_pattern = triangles +support_roof_enable = True +support_type = everywhere +support_use_towers = False +support_xy_distance = 0.7 +top_bottom_thickness = 1.2 +wall_thickness = 1.2 diff --git a/resources/quality/tinyboy/tinyboy_ra20_normal.inst.cfg b/resources/quality/tinyboy/tinyboy_ra20_normal.inst.cfg new file mode 100644 index 0000000000..366b104dfa --- /dev/null +++ b/resources/quality/tinyboy/tinyboy_ra20_normal.inst.cfg @@ -0,0 +1,61 @@ +[general] +version = 4 +name = Normal +definition = tinyboy_ra20 + +[metadata] +setting_version = 15 +type = quality +quality_type = normal +weight = 1 +global_quality = True + +[values] +acceleration_enabled = True +acceleration_print = 1800 +acceleration_travel = 3000 +adhesion_type = none +brim_width = 4.0 +cool_fan_full_at_height = 0.5 +cool_fan_speed = 100 +cool_fan_speed_0 = 100 +infill_overlap = 15 +infill_pattern = zigzag +infill_sparse_density = 25 +initial_layer_line_width_factor = 140 +jerk_enabled = True +jerk_print = 8 +jerk_travel = 10 +layer_height = 0.2 +layer_height_0 = 0.2 +material_bed_temperature = 60 +material_diameter = 1.75 +material_print_temperature = 200 +material_print_temperature_layer_0 = 0 +retract_at_layer_change = False +retraction_amount = 6 +retraction_hop = 0.075 +retraction_hop_enabled = True +retraction_hop_only_when_collides = True +retraction_min_travel = 1.5 +retraction_speed = 40 +skirt_brim_speed = 40 +skirt_gap = 5 +skirt_line_count = 3 +speed_infill = =speed_print +speed_print = 50 +speed_support = 30 +speed_topbottom = =math.ceil(speed_print * 20 / 50) +speed_travel = 100 +speed_wall = =speed_print +speed_wall_x = =speed_print +support_angle = 60 +support_enable = True +support_interface_enable = True +support_pattern = triangles +support_roof_enable = True +support_type = everywhere +support_use_towers = False +support_xy_distance = 0.7 +top_bottom_thickness = 1.2 +wall_thickness = 1.2 diff --git a/resources/texts/change_log.txt b/resources/texts/change_log.txt index 4eef21b742..8ce125d932 100644 --- a/resources/texts/change_log.txt +++ b/resources/texts/change_log.txt @@ -1,3 +1,7 @@ +[4.6.2] + +TODO + [4.6.1] Patch release to fix some bugs that emerged with 4.6.0. diff --git a/resources/themes/cura-dark/theme.json b/resources/themes/cura-dark/theme.json index 37c2462633..69bd14765a 100644 --- a/resources/themes/cura-dark/theme.json +++ b/resources/themes/cura-dark/theme.json @@ -24,6 +24,8 @@ "main_window_header_button_text_inactive": [128, 128, 128, 255], + "account_sync_state_icon": [255, 255, 255, 204], + "machine_selector_bar": [39, 44, 48, 255], "machine_selector_active": [39, 44, 48, 255], "machine_selector_printer_icon": [204, 204, 204, 255], diff --git a/resources/themes/cura-light/icons/checked.svg b/resources/themes/cura-light/icons/checked.svg index e98e2abcd7..22d1278667 100644 --- a/resources/themes/cura-light/icons/checked.svg +++ b/resources/themes/cura-light/icons/checked.svg @@ -4,9 +4,9 @@ checked Created with Sketch. - - - + + + \ No newline at end of file diff --git a/resources/themes/cura-light/icons/printer_cloud_not_available.svg b/resources/themes/cura-light/icons/printer_cloud_not_available.svg new file mode 100644 index 0000000000..248df27338 --- /dev/null +++ b/resources/themes/cura-light/icons/printer_cloud_not_available.svg @@ -0,0 +1,18 @@ + + + + Artboard Copy 4 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index b870603bd9..cab2dfe89c 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -183,6 +183,7 @@ "main_window_header_button_background_hovered": [117, 114, 159, 255], "account_widget_outline_active": [70, 66, 126, 255], + "account_sync_state_icon": [25, 25, 25, 255], "machine_selector_bar": [31, 36, 39, 255], "machine_selector_active": [68, 72, 75, 255], @@ -438,7 +439,9 @@ "monitor_shadow": [200, 200, 200, 255], "monitor_carousel_dot": [216, 216, 216, 255], - "monitor_carousel_dot_current": [119, 119, 119, 255] + "monitor_carousel_dot_current": [119, 119, 119, 255], + + "cloud_unavailable": [153, 153, 153, 255] }, "sizes": { diff --git a/resources/variants/atmat_asterion_ht_v6_0.40.inst.cfg b/resources/variants/atmat_asterion_ht_v6_0.40.inst.cfg new file mode 100644 index 0000000000..3844f79695 --- /dev/null +++ b/resources/variants/atmat_asterion_ht_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_asterion_ht + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_asterion_ht_v6_0.80.inst.cfg b/resources/variants/atmat_asterion_ht_v6_0.80.inst.cfg new file mode 100644 index 0000000000..7479e4236a --- /dev/null +++ b/resources/variants/atmat_asterion_ht_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_asterion_ht + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/resources/variants/atmat_asterion_v6_0.40.inst.cfg b/resources/variants/atmat_asterion_v6_0.40.inst.cfg new file mode 100644 index 0000000000..a395a0cd49 --- /dev/null +++ b/resources/variants/atmat_asterion_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_asterion + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_asterion_v6_0.80.inst.cfg b/resources/variants/atmat_asterion_v6_0.80.inst.cfg new file mode 100644 index 0000000000..3f6e1caaa0 --- /dev/null +++ b/resources/variants/atmat_asterion_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_asterion + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/resources/variants/atmat_galaxy_500_v6_0.40.inst.cfg b/resources/variants/atmat_galaxy_500_v6_0.40.inst.cfg new file mode 100644 index 0000000000..b5620e6f2e --- /dev/null +++ b/resources/variants/atmat_galaxy_500_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_galaxy_500 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_galaxy_500_v6_0.80.inst.cfg b/resources/variants/atmat_galaxy_500_v6_0.80.inst.cfg new file mode 100644 index 0000000000..65baba3593 --- /dev/null +++ b/resources/variants/atmat_galaxy_500_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_galaxy_500 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/resources/variants/atmat_galaxy_600_v6_0.40.inst.cfg b/resources/variants/atmat_galaxy_600_v6_0.40.inst.cfg new file mode 100644 index 0000000000..81e4314c1b --- /dev/null +++ b/resources/variants/atmat_galaxy_600_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_galaxy_600 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_galaxy_600_v6_0.80.inst.cfg b/resources/variants/atmat_galaxy_600_v6_0.80.inst.cfg new file mode 100644 index 0000000000..5aa4a89f07 --- /dev/null +++ b/resources/variants/atmat_galaxy_600_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_galaxy_600 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/resources/variants/atmat_signal_pro_300_v1_v6_0.40.inst.cfg b/resources/variants/atmat_signal_pro_300_v1_v6_0.40.inst.cfg new file mode 100644 index 0000000000..1652a6a0d9 --- /dev/null +++ b/resources/variants/atmat_signal_pro_300_v1_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_signal_pro_300_v1 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_signal_pro_300_v1_v6_0.80.inst.cfg b/resources/variants/atmat_signal_pro_300_v1_v6_0.80.inst.cfg new file mode 100644 index 0000000000..3015eaf3fc --- /dev/null +++ b/resources/variants/atmat_signal_pro_300_v1_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_signal_pro_300_v1 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/resources/variants/atmat_signal_pro_300_v2_v6_0.40.inst.cfg b/resources/variants/atmat_signal_pro_300_v2_v6_0.40.inst.cfg new file mode 100644 index 0000000000..ee85edfd4c --- /dev/null +++ b/resources/variants/atmat_signal_pro_300_v2_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_signal_pro_300_v2 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_signal_pro_300_v2_v6_0.80.inst.cfg b/resources/variants/atmat_signal_pro_300_v2_v6_0.80.inst.cfg new file mode 100644 index 0000000000..0a3dd71327 --- /dev/null +++ b/resources/variants/atmat_signal_pro_300_v2_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_signal_pro_300_v2 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/resources/variants/atmat_signal_pro_400_v1_v6_0.40.inst.cfg b/resources/variants/atmat_signal_pro_400_v1_v6_0.40.inst.cfg new file mode 100644 index 0000000000..70a31771f7 --- /dev/null +++ b/resources/variants/atmat_signal_pro_400_v1_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_signal_pro_400_v1 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_signal_pro_400_v1_v6_0.80.inst.cfg b/resources/variants/atmat_signal_pro_400_v1_v6_0.80.inst.cfg new file mode 100644 index 0000000000..eac64b24a0 --- /dev/null +++ b/resources/variants/atmat_signal_pro_400_v1_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_signal_pro_400_v1 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/resources/variants/atmat_signal_pro_400_v2_v6_0.40.inst.cfg b/resources/variants/atmat_signal_pro_400_v2_v6_0.40.inst.cfg new file mode 100644 index 0000000000..77f5cff109 --- /dev/null +++ b/resources/variants/atmat_signal_pro_400_v2_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_signal_pro_400_v2 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_signal_pro_400_v2_v6_0.80.inst.cfg b/resources/variants/atmat_signal_pro_400_v2_v6_0.80.inst.cfg new file mode 100644 index 0000000000..ff5a0e431c --- /dev/null +++ b/resources/variants/atmat_signal_pro_400_v2_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_signal_pro_400_v2 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/resources/variants/atmat_signal_pro_500_v1_v6_0.40.inst.cfg b/resources/variants/atmat_signal_pro_500_v1_v6_0.40.inst.cfg new file mode 100644 index 0000000000..0eec96f7a2 --- /dev/null +++ b/resources/variants/atmat_signal_pro_500_v1_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_signal_pro_500_v1 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_signal_pro_500_v1_v6_0.80.inst.cfg b/resources/variants/atmat_signal_pro_500_v1_v6_0.80.inst.cfg new file mode 100644 index 0000000000..966a9c208b --- /dev/null +++ b/resources/variants/atmat_signal_pro_500_v1_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_signal_pro_500_v1 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/resources/variants/atmat_signal_pro_500_v2_v6_0.40.inst.cfg b/resources/variants/atmat_signal_pro_500_v2_v6_0.40.inst.cfg new file mode 100644 index 0000000000..9961727e06 --- /dev/null +++ b/resources/variants/atmat_signal_pro_500_v2_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_signal_pro_500_v2 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_signal_pro_500_v2_v6_0.80.inst.cfg b/resources/variants/atmat_signal_pro_500_v2_v6_0.80.inst.cfg new file mode 100644 index 0000000000..15770d8b18 --- /dev/null +++ b/resources/variants/atmat_signal_pro_500_v2_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_signal_pro_500_v2 + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/resources/variants/atmat_signal_xl_v6_0.40.inst.cfg b/resources/variants/atmat_signal_xl_v6_0.40.inst.cfg new file mode 100644 index 0000000000..9e2d923a60 --- /dev/null +++ b/resources/variants/atmat_signal_xl_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_signal_xl + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_signal_xl_v6_0.80.inst.cfg b/resources/variants/atmat_signal_xl_v6_0.80.inst.cfg new file mode 100644 index 0000000000..068f17f2b1 --- /dev/null +++ b/resources/variants/atmat_signal_xl_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_signal_xl + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/resources/variants/atmat_signal_xxl_v6_0.40.inst.cfg b/resources/variants/atmat_signal_xxl_v6_0.40.inst.cfg new file mode 100644 index 0000000000..017758993e --- /dev/null +++ b/resources/variants/atmat_signal_xxl_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_signal_xxl + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_signal_xxl_v6_0.80.inst.cfg b/resources/variants/atmat_signal_xxl_v6_0.80.inst.cfg new file mode 100644 index 0000000000..eb9e3453fa --- /dev/null +++ b/resources/variants/atmat_signal_xxl_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_signal_xxl + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/resources/variants/atmat_signal_xxxl_v6_0.40.inst.cfg b/resources/variants/atmat_signal_xxxl_v6_0.40.inst.cfg new file mode 100644 index 0000000000..7e49a712f9 --- /dev/null +++ b/resources/variants/atmat_signal_xxxl_v6_0.40.inst.cfg @@ -0,0 +1,12 @@ +[general] +name = V6 0.40mm +version = 4 +definition = atmat_signal_xxxl + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.4 diff --git a/resources/variants/atmat_signal_xxxl_v6_0.80.inst.cfg b/resources/variants/atmat_signal_xxxl_v6_0.80.inst.cfg new file mode 100644 index 0000000000..185505e7fc --- /dev/null +++ b/resources/variants/atmat_signal_xxxl_v6_0.80.inst.cfg @@ -0,0 +1,13 @@ +[general] +name = V6 0.80mm +version = 4 +definition = atmat_signal_xxxl + +[metadata] +setting_version = 15 +type = variant +hardware_type = nozzle + +[values] +machine_nozzle_size = 0.8 +support_angle = 55 diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000..abdd38998e Binary files /dev/null and b/screenshot.png differ diff --git a/scripts/check_gcode_buffer.py b/scripts/check_gcode_buffer.py index 2024ce2214..321b2439c0 100755 --- a/scripts/check_gcode_buffer.py +++ b/scripts/check_gcode_buffer.py @@ -31,16 +31,19 @@ MACHINE_MAX_JERK_E = 5 MACHINE_MINIMUM_FEEDRATE = 0.001 MACHINE_ACCELERATION = 3000 -## Gets the code and number from the given g-code line. + def get_code_and_num(gcode_line: str) -> Tuple[str, str]: + """Gets the code and number from the given g-code line.""" + gcode_line = gcode_line.strip() cmd_code = gcode_line[0].upper() cmd_num = str(gcode_line[1:]) return cmd_code, cmd_num -## Fetches arguments such as X1 Y2 Z3 from the given part list and returns a -# dict. + def get_value_dict(parts: List[str]) -> Dict[str, str]: + """Fetches arguments such as X1 Y2 Z3 from the given part list and returns a dict""" + value_dict = {} for p in parts: p = p.strip() @@ -63,39 +66,49 @@ def calc_distance(pos1, pos2): distance = math.sqrt(distance) return distance -## Given the initial speed, the target speed, and the acceleration, calculate -# the distance that's neede for the acceleration to finish. + def calc_acceleration_distance(init_speed: float, target_speed: float, acceleration: float) -> float: + """Given the initial speed, the target speed, and the acceleration + + calculate the distance that's neede for the acceleration to finish. + """ if acceleration == 0: return 0.0 return (target_speed ** 2 - init_speed ** 2) / (2 * acceleration) -## Gives the time it needs to accelerate from an initial speed to reach a final -# distance. + def calc_acceleration_time_from_distance(initial_feedrate: float, distance: float, acceleration: float) -> float: + """Gives the time it needs to accelerate from an initial speed to reach a final distance.""" + discriminant = initial_feedrate ** 2 - 2 * acceleration * -distance #If the discriminant is negative, we're moving in the wrong direction. #Making the discriminant 0 then gives the extremum of the parabola instead of the intersection. discriminant = max(0, discriminant) return (-initial_feedrate + math.sqrt(discriminant)) / acceleration -## Calculates the point at which you must start braking. -# -# This gives the distance from the start of a line at which you must start -# decelerating (at a rate of `-acceleration`) if you started at speed -# `initial_feedrate` and accelerated until this point and want to end at the -# `final_feedrate` after a total travel of `distance`. This can be used to -# compute the intersection point between acceleration and deceleration in the -# cases where the trapezoid has no plateau (i.e. never reaches maximum speed). + def calc_intersection_distance(initial_feedrate: float, final_feedrate: float, acceleration: float, distance: float) -> float: + """Calculates the point at which you must start braking. + + This gives the distance from the start of a line at which you must start + decelerating (at a rate of `-acceleration`) if you started at speed + `initial_feedrate` and accelerated until this point and want to end at the + `final_feedrate` after a total travel of `distance`. This can be used to + compute the intersection point between acceleration and deceleration in the + cases where the trapezoid has no plateau (i.e. never reaches maximum speed). + """ + if acceleration == 0: return 0 return (2 * acceleration * distance - initial_feedrate * initial_feedrate + final_feedrate * final_feedrate) / (4 * acceleration) -## Calculates the maximum speed that is allowed at this point when you must be -# able to reach target_velocity using the acceleration within the allotted -# distance. + def calc_max_allowable_speed(acceleration: float, target_velocity: float, distance: float) -> float: + """Calculates the maximum speed that is allowed at this point when you must be + able to reach target_velocity using the acceleration within the allotted + distance. + """ + return math.sqrt(target_velocity * target_velocity - 2 * acceleration * distance) @@ -130,10 +143,12 @@ class Command: self._delta = [0, 0, 0] self._abs_delta = [0, 0, 0] - ## Calculate the velocity-time trapezoid function for this move. - # - # Each move has a three-part function mapping time to velocity. def calculate_trapezoid(self, entry_factor, exit_factor): + """Calculate the velocity-time trapezoid function for this move. + + Each move has a three-part function mapping time to velocity. + """ + initial_feedrate = self._nominal_feedrate * entry_factor final_feedrate = self._nominal_feedrate * exit_factor @@ -169,9 +184,9 @@ class Command: return self._cmd_str.strip() + " ; --- " + info + os.linesep - ## Estimates the execution time of this command and calculates the state - # after this command is executed. def parse(self) -> None: + """Estimates the execution time of this command and calculates the state after this command is executed.""" + line = self._cmd_str.strip() if not line: self._is_empty = True diff --git a/scripts/lionbridge_import.py b/scripts/lionbridge_import.py index 83f53403ea..0a7b63e9ac 100644 --- a/scripts/lionbridge_import.py +++ b/scripts/lionbridge_import.py @@ -9,14 +9,16 @@ import os.path #To find files from the source and the destination path. cura_files = {"cura", "fdmprinter.def.json", "fdmextruder.def.json"} uranium_files = {"uranium"} -## Imports translation files from Lionbridge. -# -# Lionbridge has a bit of a weird export feature. It exports it to the same -# file type as what we imported, so that's a .pot file. However this .pot file -# only contains the translations, so the header is completely empty. We need -# to merge those translations into our existing files so that the header is -# preserved. def lionbridge_import(source: str) -> None: + """Imports translation files from Lionbridge. + + Lionbridge has a bit of a weird export feature. It exports it to the same + file type as what we imported, so that's a .pot file. However this .pot file + only contains the translations, so the header is completely empty. We need + to merge those translations into our existing files so that the header is + preserved. + """ + print("Importing from:", source) print("Importing to Cura:", destination_cura()) print("Importing to Uranium:", destination_uranium()) @@ -43,14 +45,20 @@ def lionbridge_import(source: str) -> None: with io.open(destination_file, "w", encoding = "utf8") as f: f.write(result) -## Gets the destination path to copy the translations for Cura to. -# \return Destination path for Cura. + def destination_cura() -> str: + """Gets the destination path to copy the translations for Cura to. + + :return: Destination path for Cura. + """ return os.path.abspath(os.path.join(__file__, "..", "..", "resources", "i18n")) -## Gets the destination path to copy the translations for Uranium to. -# \return Destination path for Uranium. + def destination_uranium() -> str: + """Gets the destination path to copy the translations for Uranium to. + + :return: Destination path for Uranium. + """ try: import UM except ImportError: @@ -64,11 +72,14 @@ def destination_uranium() -> str: raise Exception("Can't find Uranium. Please put UM on the PYTHONPATH or put the Uranium folder next to the Cura folder. Looked for: " + absolute_path) return os.path.abspath(os.path.join(UM.__file__, "..", "..", "resources", "i18n")) -## Merges translations from the source file into the destination file if they -# were missing in the destination file. -# \param source The contents of the source .po file. -# \param destination The contents of the destination .po file. + def merge(source: str, destination: str) -> str: + """Merges translations from the source file into the destination file if they + + were missing in the destination file. + :param source: The contents of the source .po file. + :param destination: The contents of the destination .po file. + """ result_lines = [] last_destination = { "msgctxt": "\"\"\n", @@ -119,11 +130,14 @@ def merge(source: str, destination: str) -> str: result_lines.append(line) #This line itself. return "\n".join(result_lines) -## Finds a translation in the source file. -# \param source The contents of the source .po file. -# \param msgctxt The ctxt of the translation to find. -# \param msgid The id of the translation to find. + def find_translation(source: str, msgctxt: str, msgid: str) -> str: + """Finds a translation in the source file. + + :param source: The contents of the source .po file. + :param msgctxt: The ctxt of the translation to find. + :param msgid: The id of the translation to find. + """ last_source = { "msgctxt": "\"\"\n", "msgid": "\"\"\n", diff --git a/tests/Machines/TestMachineNode.py b/tests/Machines/TestMachineNode.py index 50d7bdafa0..cb1d4005ec 100644 --- a/tests/Machines/TestMachineNode.py +++ b/tests/Machines/TestMachineNode.py @@ -26,12 +26,14 @@ def container_registry(): result.findContainersMetadata = MagicMock(return_value = [metadata_dict]) return result -## Creates a machine node without anything underneath it. No sub-nodes. -# -# For testing stuff with machine nodes without testing _loadAll(). You'll need -# to add subnodes manually in your test. @pytest.fixture def empty_machine_node(): + """Creates a machine node without anything underneath it. No sub-nodes. + + For testing stuff with machine nodes without testing _loadAll(). You'll need + to add subnodes manually in your test. + """ + empty_container_registry = MagicMock() empty_container_registry.findContainersMetadata = MagicMock(return_value = [metadata_dict]) # Still contain the MachineNode's own metadata for the constructor. empty_container_registry.findInstanceContainersMetadata = MagicMock(return_value = []) @@ -77,9 +79,13 @@ def test_metadataProperties(container_registry): assert node.preferred_material == metadata_dict["preferred_material"] assert node.preferred_quality_type == metadata_dict["preferred_quality_type"] -## Test getting quality groups when there are quality profiles available for -# the requested configurations on two extruders. + def test_getQualityGroupsBothExtrudersAvailable(empty_machine_node): + """Test getting quality groups when there are quality profiles available for + + the requested configurations on two extruders. + """ + # Prepare a tree inside the machine node. extruder_0_node = MagicMock(quality_type = "quality_type_1") extruder_1_node = MagicMock(quality_type = "quality_type_1") # Same quality type, so this is the one that can be returned. @@ -121,12 +127,15 @@ def test_getQualityGroupsBothExtrudersAvailable(empty_machine_node): assert result["quality_type_1"].name == global_node.getMetaDataEntry("name", "Unnamed Profile") assert result["quality_type_1"].quality_type == "quality_type_1" -## Test the "is_available" flag on quality groups. -# -# If a profile is available for a quality type on an extruder but not on all -# extruders, there should be a quality group for it but it should not be made -# available. + def test_getQualityGroupsAvailability(empty_machine_node): + """Test the "is_available" flag on quality groups. + + If a profile is available for a quality type on an extruder but not on all + extruders, there should be a quality group for it but it should not be made + available. + """ + # Prepare a tree inside the machine node. extruder_0_both = MagicMock(quality_type = "quality_type_both") # This quality type is available for both extruders. extruder_1_both = MagicMock(quality_type = "quality_type_both") diff --git a/tests/Machines/TestQualityNode.py b/tests/Machines/TestQualityNode.py index ffe897d203..0501450b5d 100644 --- a/tests/Machines/TestQualityNode.py +++ b/tests/Machines/TestQualityNode.py @@ -6,7 +6,7 @@ import pytest from cura.Machines.QualityNode import QualityNode -## Metadata for hypothetical containers that get put in the registry. +# Metadata for hypothetical containers that get put in the registry. metadatas = [ { "id": "matching_intent", # Matches our query. diff --git a/tests/Machines/TestVariantNode.py b/tests/Machines/TestVariantNode.py index 9a0213ef99..7f06ad468c 100644 --- a/tests/Machines/TestVariantNode.py +++ b/tests/Machines/TestVariantNode.py @@ -49,12 +49,14 @@ def machine_node(): mocked_machine_node.preferred_material = "preferred_material" return mocked_machine_node -## Constructs a variant node without any subnodes. -# -# This is useful for performing tests on VariantNode without being dependent -# on how _loadAll works. @pytest.fixture def empty_variant_node(machine_node): + """Constructs a variant node without any subnodes. + + This is useful for performing tests on VariantNode without being dependent + on how _loadAll works. + """ + container_registry = MagicMock( findContainersMetadata = MagicMock(return_value = [{"name": "test variant name"}]) ) @@ -132,9 +134,12 @@ def test_materialAdded_update(container_registry, machine_node, metadata, change for key in changed_material_list: assert original_material_nodes[key] != variant_node.materials[key] -## Tests the preferred material when the exact base file is available in the -# materials list for this node. + def test_preferredMaterialExactMatch(empty_variant_node): + """Tests the preferred material when the exact base file is available in the + + materials list for this node. + """ empty_variant_node.materials = { "some_different_material": MagicMock(getMetaDataEntry = lambda x: 3), "preferred_material": MagicMock(getMetaDataEntry = lambda x: 3) # Exact match. @@ -143,9 +148,12 @@ def test_preferredMaterialExactMatch(empty_variant_node): assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material"], "It should match exactly on this one since it's the preferred material." -## Tests the preferred material when a submaterial is available in the -# materials list for this node. + def test_preferredMaterialSubmaterial(empty_variant_node): + """Tests the preferred material when a submaterial is available in the + + materials list for this node. + """ empty_variant_node.materials = { "some_different_material": MagicMock(getMetaDataEntry = lambda x: 3), "preferred_material_base_file_aa04": MagicMock(getMetaDataEntry = lambda x: 3) # This is a submaterial of the preferred material. @@ -154,9 +162,10 @@ def test_preferredMaterialSubmaterial(empty_variant_node): assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material_base_file_aa04"], "It should match on the submaterial just as well." -## Tests the preferred material matching on the approximate diameter of the -# filament. + def test_preferredMaterialDiameter(empty_variant_node): + """Tests the preferred material matching on the approximate diameter of the filament. + """ empty_variant_node.materials = { "some_different_material": MagicMock(getMetaDataEntry = lambda x: 3), "preferred_material_wrong_diameter": MagicMock(getMetaDataEntry = lambda x: 2), # Approximate diameter is 2 instead of 3. @@ -166,18 +175,22 @@ def test_preferredMaterialDiameter(empty_variant_node): assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material_correct_diameter"], "It should match only on the material with correct diameter." -## Tests the preferred material matching on a different material if the -# diameter is wrong. + def test_preferredMaterialDiameterNoMatch(empty_variant_node): + """Tests the preferred material matching on a different material if the diameter is wrong.""" + empty_variant_node.materials = collections.OrderedDict() empty_variant_node.materials["some_different_material"] = MagicMock(getMetaDataEntry = lambda x: 3) # This one first so that it gets iterated over first. empty_variant_node.materials["preferred_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # Wrong diameter. assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["some_different_material"], "It should match on another material with the correct diameter if the preferred one is unavailable." -## Tests that the material diameter is considered more important to match than -# the preferred diameter. + def test_preferredMaterialDiameterPreference(empty_variant_node): + """Tests that the material diameter is considered more important to match than + the preferred diameter. + """ + empty_variant_node.materials = collections.OrderedDict() empty_variant_node.materials["some_different_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # This one first so that it gets iterated over first. empty_variant_node.materials["preferred_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # Matches on ID but not diameter. diff --git a/tests/Settings/MockContainer.py b/tests/Settings/MockContainer.py index 0400359154..9c20f55405 100644 --- a/tests/Settings/MockContainer.py +++ b/tests/Settings/MockContainer.py @@ -5,18 +5,21 @@ import UM.PluginObject from UM.Signal import Signal -## Fake container class to add to the container registry. -# -# This allows us to test the container registry without testing the container -# class. If something is wrong in the container class it won't influence this -# test. - class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): - ## Initialise a new definition container. - # - # The container will have the specified ID and all metadata in the - # provided dictionary. + """Fake container class to add to the container registry. + + This allows us to test the container registry without testing the container + class. If something is wrong in the container class it won't influence this + test. + """ + def __init__(self, metadata = None): + """Initialise a new definition container. + + The container will have the specified ID and all metadata in the + provided dictionary. + """ + super().__init__() if metadata is None: self._metadata = {} @@ -24,55 +27,69 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): self._metadata = metadata self._plugin_id = "MockContainerPlugin" - ## Gets the ID that was provided at initialisation. - # - # \return The ID of the container. def getId(self): + """Gets the ID that was provided at initialisation. + + :return: The ID of the container. + """ + return self._metadata["id"] - ## Gets all metadata of this container. - # - # This returns the metadata dictionary that was provided in the - # constructor of this mock container. - # - # \return The metadata for this container. def getMetaData(self): + """Gets all metadata of this container. + + This returns the metadata dictionary that was provided in the + constructor of this mock container. + + :return: The metadata for this container. + """ + return self._metadata - ## Gets a metadata entry from the metadata dictionary. - # - # \param key The key of the metadata entry. - # \return The value of the metadata entry, or None if there is no such - # entry. def getMetaDataEntry(self, entry, default = None): + """Gets a metadata entry from the metadata dictionary. + + :param key: The key of the metadata entry. + :return: The value of the metadata entry, or None if there is no such + entry. + """ + if entry in self._metadata: return self._metadata[entry] return default - ## Gets a human-readable name for this container. - # \return The name from the metadata, or "MockContainer" if there was no - # name provided. def getName(self): + """Gets a human-readable name for this container. + + :return: The name from the metadata, or "MockContainer" if there was no + name provided. + """ return self._metadata.get("name", "MockContainer") - ## Get whether a container stack is enabled or not. - # \return Always returns True. @property def isEnabled(self): + """Get whether a container stack is enabled or not. + + :return: Always returns True. + """ return True - ## Get whether the container item is stored on a read only location in the filesystem. - # - # \return Always returns False def isReadOnly(self): + """Get whether the container item is stored on a read only location in the filesystem. + + :return: Always returns False + """ + return False - ## Mock get path def getPath(self): + """Mock get path""" + return "/path/to/the/light/side" - ## Mock set path def setPath(self, path): + """Mock set path""" + pass def getAllKeys(self): @@ -91,31 +108,38 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): return None - ## Get the value of a container item. - # - # Since this mock container cannot contain any items, it always returns - # None. - # - # \return Always returns None. def getValue(self, key): + """Get the value of a container item. + + Since this mock container cannot contain any items, it always returns None. + + :return: Always returns None. + """ + pass - ## Get whether the container item has a specific property. - # - # This method is not implemented in the mock container. def hasProperty(self, key, property_name): + """Get whether the container item has a specific property. + + This method is not implemented in the mock container. + """ + return key in self.items - ## Serializes the container to a string representation. - # - # This method is not implemented in the mock container. def serialize(self, ignored_metadata_keys = None): + """Serializes the container to a string representation. + + This method is not implemented in the mock container. + """ + raise NotImplementedError() - ## Deserializes the container from a string representation. - # - # This method is not implemented in the mock container. def deserialize(self, serialized, file_name: Optional[str] = None): + """Deserializes the container from a string representation. + + This method is not implemented in the mock container. + """ + raise NotImplementedError() @classmethod @@ -129,6 +153,9 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): def isDirty(self): return True + def setDirty(self, dirty): + pass + metaDataChanged = Signal() propertyChanged = Signal() containersChanged = Signal() diff --git a/tests/Settings/TestContainerManager.py b/tests/Settings/TestContainerManager.py index ff23b727e6..19ade68f68 100644 --- a/tests/Settings/TestContainerManager.py +++ b/tests/Settings/TestContainerManager.py @@ -14,6 +14,7 @@ class TestContainerManager(TestCase): self._application = MagicMock() self._container_registry = MagicMock() self._machine_manager = MagicMock() + self._machine_manager.activeMachine.extruderList = [MagicMock(name="Left Extruder Mock"), MagicMock(name="Right Extruder Mock")] self._mocked_mime = MagicMock() self._mocked_mime.preferredSuffix = "omg" diff --git a/tests/Settings/TestCuraContainerRegistry.py b/tests/Settings/TestCuraContainerRegistry.py index 8bd6fe0ccb..cd8a9fd49d 100644 --- a/tests/Settings/TestCuraContainerRegistry.py +++ b/tests/Settings/TestCuraContainerRegistry.py @@ -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. import os #To find the directory with test files and find the test files. @@ -42,8 +42,9 @@ def test_createUniqueName(container_registry): assert container_registry.createUniqueName("user", "test", "", "nope") == "nope" -## Tests whether addContainer properly converts to ExtruderStack. def test_addContainerExtruderStack(container_registry, definition_container, definition_changes_container): + """Tests whether addContainer properly converts to ExtruderStack.""" + container_registry.addContainer(definition_container) container_registry.addContainer(definition_changes_container) @@ -61,8 +62,9 @@ def test_addContainerExtruderStack(container_registry, definition_container, def assert type(mock_super_add_container.call_args_list[0][0][0]) == ExtruderStack -## Tests whether addContainer properly converts to GlobalStack. def test_addContainerGlobalStack(container_registry, definition_container, definition_changes_container): + """Tests whether addContainer properly converts to GlobalStack.""" + container_registry.addContainer(definition_container) container_registry.addContainer(definition_changes_container) @@ -249,14 +251,13 @@ def test_importProfileEmptyFileName(container_registry): mocked_application = unittest.mock.MagicMock(name = "application") -mocked_plugin_registry = unittest.mock.MagicMock(name="mocked_plugin_registry") +mocked_plugin_registry = unittest.mock.MagicMock(name = "mocked_plugin_registry") @unittest.mock.patch("UM.Application.Application.getInstance", unittest.mock.MagicMock(return_value = mocked_application)) @unittest.mock.patch("UM.PluginRegistry.PluginRegistry.getInstance", unittest.mock.MagicMock(return_value = mocked_plugin_registry)) class TestImportProfile: - mocked_global_stack = unittest.mock.MagicMock(name="global stack") - mocked_global_stack.extruders = {0: unittest.mock.MagicMock(name="extruder stack")} - mocked_global_stack.getId = unittest.mock.MagicMock(return_value="blarg") + mocked_global_stack = unittest.mock.MagicMock(name = "global stack") + mocked_global_stack.getId = unittest.mock.MagicMock(return_value = "blarg") mocked_profile_reader = unittest.mock.MagicMock() mocked_plugin_registry.getPluginObject = unittest.mock.MagicMock(return_value=mocked_profile_reader) diff --git a/tests/Settings/TestDefinitionContainer.py b/tests/Settings/TestDefinitionContainer.py index 2f2b343338..d434066d10 100644 --- a/tests/Settings/TestDefinitionContainer.py +++ b/tests/Settings/TestDefinitionContainer.py @@ -12,7 +12,7 @@ from unittest.mock import patch, MagicMock import UM.Settings.ContainerRegistry #To create empty instance containers. import UM.Settings.ContainerStack #To set the container registry the container stacks use. from UM.Settings.DefinitionContainer import DefinitionContainer #To check against the class of DefinitionContainer. - +from UM.VersionUpgradeManager import FilesDataUpdateResult from UM.Resources import Resources Resources.addSearchPath(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "resources"))) @@ -36,6 +36,7 @@ def definition_container(): assert result.getId() == uid return result + @pytest.mark.parametrize("file_path", definition_filepaths) def test_definitionIds(file_path): """ @@ -45,6 +46,7 @@ def test_definitionIds(file_path): definition_id = os.path.basename(file_path).split(".")[0] assert " " not in definition_id # Definition IDs are not allowed to have spaces. + @pytest.mark.parametrize("file_path", definition_filepaths) def test_noCategory(file_path): """ @@ -57,20 +59,21 @@ def test_noCategory(file_path): metadata = DefinitionContainer.deserializeMetadata(json, "test_container_id") assert "category" not in metadata[0] -## Tests all definition containers + @pytest.mark.parametrize("file_path", machine_filepaths) def test_validateMachineDefinitionContainer(file_path, definition_container): + """Tests all definition containers""" + file_name = os.path.basename(file_path) if file_name == "fdmprinter.def.json" or file_name == "fdmextruder.def.json": return # Stop checking, these are root files. - from UM.VersionUpgradeManager import FilesDataUpdateResult - mocked_vum = MagicMock() mocked_vum.updateFilesData = lambda ct, v, fdl, fnl: FilesDataUpdateResult(ct, v, fdl, fnl) with patch("UM.VersionUpgradeManager.VersionUpgradeManager.getInstance", MagicMock(return_value = mocked_vum)): assertIsDefinitionValid(definition_container, file_path) + def assertIsDefinitionValid(definition_container, file_path): with open(file_path, encoding = "utf-8") as data: json = data.read() @@ -85,13 +88,16 @@ def assertIsDefinitionValid(definition_container, file_path): if "platform_texture" in metadata[0]: assert metadata[0]["platform_texture"] in all_images -## Tests whether setting values are not being hidden by parent containers. -# -# When a definition container defines a "default_value" but inherits from a -# definition that defines a "value", the "default_value" is ineffective. This -# test fails on those things. + @pytest.mark.parametrize("file_path", definition_filepaths) def test_validateOverridingDefaultValue(file_path: str): + """Tests whether setting values are not being hidden by parent containers. + + When a definition container defines a "default_value" but inherits from a + definition that defines a "value", the "default_value" is ineffective. This + test fails on those things. + """ + with open(file_path, encoding = "utf-8") as f: doc = json.load(f) @@ -107,12 +113,14 @@ def test_validateOverridingDefaultValue(file_path: str): faulty_keys.add(key) assert not faulty_keys, "Unnecessary default_values for {faulty_keys} in {file_name}".format(faulty_keys = sorted(faulty_keys), file_name = file_path) # If there is a value in the parent settings, then the default_value is not effective. -## Get all settings and their properties from a definition we're inheriting -# from. -# \param definition_id The definition we're inheriting from. -# \return A dictionary of settings by key. Each setting is a dictionary of -# properties. + def getInheritedSettings(definition_id: str) -> Dict[str, Any]: + """Get all settings and their properties from a definition we're inheriting from. + + :param definition_id: The definition we're inheriting from. + :return: A dictionary of settings by key. Each setting is a dictionary of properties. + """ + definition_path = os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions", definition_id + ".def.json") with open(definition_path, encoding = "utf-8") as f: doc = json.load(f) @@ -127,13 +135,15 @@ def getInheritedSettings(definition_id: str) -> Dict[str, Any]: return result -## Put all settings in the main dictionary rather than in children dicts. -# \param settings Nested settings. The keys are the setting IDs. The values -# are dictionaries of properties per setting, including the "children" -# property. -# \return A dictionary of settings by key. Each setting is a dictionary of -# properties. + def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]: + """Put all settings in the main dictionary rather than in children dicts. + + :param settings: Nested settings. The keys are the setting IDs. The values + are dictionaries of properties per setting, including the "children" property. + :return: A dictionary of settings by key. Each setting is a dictionary of properties. + """ + result = {} for entry, contents in settings.items(): if "children" in contents: @@ -142,12 +152,16 @@ def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]: result[entry] = contents return result -## Make one dictionary override the other. Nested dictionaries override each -# other in the same way. -# \param base A dictionary of settings that will get overridden by the other. -# \param overrides A dictionary of settings that will override the other. -# \return Combined setting data. + def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, Any]: + """Make one dictionary override the other. Nested dictionaries override each + + other in the same way. + :param base: A dictionary of settings that will get overridden by the other. + :param overrides: A dictionary of settings that will override the other. + :return: Combined setting data. + """ + result = {} result.update(base) for key, val in overrides.items(): @@ -161,21 +175,27 @@ def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, An result[key] = val return result -## Verifies that definition contains don't have an ID field. -# -# ID fields are legacy. They should not be used any more. This is legacy that -# people don't seem to be able to get used to. + @pytest.mark.parametrize("file_path", definition_filepaths) def test_noId(file_path: str): + """Verifies that definition contains don't have an ID field. + + ID fields are legacy. They should not be used any more. This is legacy that + people don't seem to be able to get used to. + """ + with open(file_path, encoding = "utf-8") as f: doc = json.load(f) assert "id" not in doc, "Definitions should not have an ID field." -## Verifies that extruders say that they work on the same extruder_nr as what -# is listed in their machine definition. + @pytest.mark.parametrize("file_path", extruder_filepaths) def test_extruderMatch(file_path: str): + """ + Verifies that extruders say that they work on the same extruder_nr as what is listed in their machine definition. + """ + extruder_id = os.path.basename(file_path).split(".")[0] with open(file_path, encoding = "utf-8") as f: doc = json.load(f) diff --git a/tests/Settings/TestExtruderStack.py b/tests/Settings/TestExtruderStack.py index 73d5f583b3..c5ecdea5c4 100644 --- a/tests/Settings/TestExtruderStack.py +++ b/tests/Settings/TestExtruderStack.py @@ -14,11 +14,13 @@ from cura.Settings.Exceptions import InvalidContainerError, InvalidOperationErro from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.cura_empty_instance_containers import empty_container -## Gets an instance container with a specified container type. -# -# \param container_type The type metadata for the instance container. -# \return An instance container instance. def getInstanceContainer(container_type) -> InstanceContainer: + """Gets an instance container with a specified container type. + + :param container_type: The type metadata for the instance container. + :return: An instance container instance. + """ + container = InstanceContainer(container_id = "InstanceContainer") container.setMetaDataEntry("type", container_type) return container @@ -32,10 +34,12 @@ class InstanceContainerSubClass(InstanceContainer): super().__init__(container_id = "SubInstanceContainer") self.setMetaDataEntry("type", container_type) -#############################START OF TEST CASES################################ +############################START OF TEST CASES################################ + -## Tests whether adding a container is properly forbidden. def test_addContainer(extruder_stack): + """Tests whether adding a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): extruder_stack.addContainer(unittest.mock.MagicMock()) @@ -164,8 +168,10 @@ def test_constrainDefinitionInvalid(container, extruder_stack): def test_constrainDefinitionValid(container, extruder_stack): extruder_stack.definition = container #Should not give an error. -## Tests whether deserialising completes the missing containers with empty ones. + def test_deserializeCompletesEmptyContainers(extruder_stack): + """Tests whether deserialising completes the missing containers with empty ones.""" + extruder_stack._containers = [DefinitionContainer(container_id = "definition"), extruder_stack.definitionChanges] #Set the internal state of this stack manually. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -179,8 +185,10 @@ def test_deserializeCompletesEmptyContainers(extruder_stack): continue assert extruder_stack.getContainer(container_type_index) == empty_container #All others need to be empty. -## Tests whether an instance container with the wrong type gets removed when deserialising. + def test_deserializeRemovesWrongInstanceContainer(extruder_stack): + """Tests whether an instance container with the wrong type gets removed when deserialising.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "wrong type") extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -189,8 +197,10 @@ def test_deserializeRemovesWrongInstanceContainer(extruder_stack): assert extruder_stack.quality == extruder_stack._empty_instance_container #Replaced with empty. -## Tests whether a container with the wrong class gets removed when deserialising. + def test_deserializeRemovesWrongContainerClass(extruder_stack): + """Tests whether a container with the wrong class gets removed when deserialising.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = DefinitionContainer(container_id = "wrong class") extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -199,16 +209,20 @@ def test_deserializeRemovesWrongContainerClass(extruder_stack): assert extruder_stack.quality == extruder_stack._empty_instance_container #Replaced with empty. -## Tests whether an instance container in the definition spot results in an error. + def test_deserializeWrongDefinitionClass(extruder_stack): + """Tests whether an instance container in the definition spot results in an error.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = getInstanceContainer(container_type = "definition") #Correct type but wrong class. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. with pytest.raises(UM.Settings.ContainerStack.InvalidContainerStackError): #Must raise an error that there is no definition container. extruder_stack.deserialize("") -## Tests whether an instance container with the wrong type is moved into the correct slot by deserialising. + def test_deserializeMoveInstanceContainer(extruder_stack): + """Tests whether an instance container with the wrong type is moved into the correct slot by deserialising.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "material") #Not in the correct spot. extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -218,8 +232,10 @@ def test_deserializeMoveInstanceContainer(extruder_stack): assert extruder_stack.quality == empty_container assert extruder_stack.material != empty_container -## Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising. + def test_deserializeMoveDefinitionContainer(extruder_stack): + """Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Material] = DefinitionContainer(container_id = "some definition") #Not in the correct spot. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -228,9 +244,10 @@ def test_deserializeMoveDefinitionContainer(extruder_stack): assert extruder_stack.material == empty_container assert extruder_stack.definition != empty_container -## Tests whether getProperty properly applies the stack-like behaviour on its containers. + def test_getPropertyFallThrough(global_stack, extruder_stack): - # ExtruderStack.setNextStack calls registerExtruder for backward compatibility, but we do not need a complete extruder manager + """Tests whether getProperty properly applies the stack-like behaviour on its containers.""" + ExtruderManager._ExtruderManager__instance = unittest.mock.MagicMock() #A few instance container mocks to put in the stack. @@ -273,13 +290,17 @@ def test_getPropertyFallThrough(global_stack, extruder_stack): extruder_stack.userChanges = mock_layer_heights[container_indices.UserChanges] assert extruder_stack.getProperty("layer_height", "value") == container_indices.UserChanges -## Tests whether inserting a container is properly forbidden. + def test_insertContainer(extruder_stack): + """Tests whether inserting a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): extruder_stack.insertContainer(0, unittest.mock.MagicMock()) -## Tests whether removing a container is properly forbidden. + def test_removeContainer(extruder_stack): + """Tests whether removing a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): extruder_stack.removeContainer(unittest.mock.MagicMock()) diff --git a/tests/Settings/TestGlobalStack.py b/tests/Settings/TestGlobalStack.py index c1044c9de6..ab9c034e24 100755 --- a/tests/Settings/TestGlobalStack.py +++ b/tests/Settings/TestGlobalStack.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 pytest #This module contains unit tests. @@ -16,11 +16,13 @@ import UM.Settings.SettingDefinition #To add settings to the definition. from cura.Settings.cura_empty_instance_containers import empty_container -## Gets an instance container with a specified container type. -# -# \param container_type The type metadata for the instance container. -# \return An instance container instance. def getInstanceContainer(container_type) -> InstanceContainer: + """Gets an instance container with a specified container type. + + :param container_type: The type metadata for the instance container. + :return: An instance container instance. + """ + container = InstanceContainer(container_id = "InstanceContainer") container.setMetaDataEntry("type", container_type) return container @@ -37,41 +39,43 @@ class InstanceContainerSubClass(InstanceContainer): self.setMetaDataEntry("type", container_type) -#############################START OF TEST CASES################################ +############################START OF TEST CASES################################ -## Tests whether adding a container is properly forbidden. def test_addContainer(global_stack): + """Tests whether adding a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): global_stack.addContainer(unittest.mock.MagicMock()) -## Tests adding extruders to the global stack. def test_addExtruder(global_stack): + """Tests adding extruders to the global stack.""" + mock_definition = unittest.mock.MagicMock() mock_definition.getProperty = lambda key, property, context = None: 2 if key == "machine_extruder_count" and property == "value" else None with unittest.mock.patch("cura.Settings.CuraContainerStack.DefinitionContainer", unittest.mock.MagicMock): global_stack.definition = mock_definition - assert len(global_stack.extruders) == 0 + assert len(global_stack.extruderList) == 0 first_extruder = unittest.mock.MagicMock() first_extruder.getMetaDataEntry = lambda key: 0 if key == "position" else None with unittest.mock.patch("cura.Settings.CuraContainerStack.DefinitionContainer", unittest.mock.MagicMock): global_stack.addExtruder(first_extruder) - assert len(global_stack.extruders) == 1 - assert global_stack.extruders[0] == first_extruder + assert len(global_stack.extruderList) == 1 + assert global_stack.extruderList[0] == first_extruder second_extruder = unittest.mock.MagicMock() second_extruder.getMetaDataEntry = lambda key: 1 if key == "position" else None with unittest.mock.patch("cura.Settings.CuraContainerStack.DefinitionContainer", unittest.mock.MagicMock): global_stack.addExtruder(second_extruder) - assert len(global_stack.extruders) == 2 - assert global_stack.extruders[1] == second_extruder + assert len(global_stack.extruderList) == 2 + assert global_stack.extruderList[1] == second_extruder # Disabled for now for Custom FDM Printer # with unittest.mock.patch("cura.Settings.CuraContainerStack.DefinitionContainer", unittest.mock.MagicMock): # with pytest.raises(TooManyExtrudersError): #Should be limited to 2 extruders because of machine_extruder_count. # global_stack.addExtruder(unittest.mock.MagicMock()) - assert len(global_stack.extruders) == 2 #Didn't add the faulty extruder. + assert len(global_stack.extruderList) == 2 # Didn't add the faulty extruder. #Tests setting user changes profiles to invalid containers. @@ -213,9 +217,12 @@ def test_constrainDefinitionValid(container, global_stack): global_stack.definition = container #Should not give an error. -## Tests whether deserialising completes the missing containers with empty ones. The initial containers are just the -# definition and the definition_changes (that cannot be empty after CURA-5281) def test_deserializeCompletesEmptyContainers(global_stack): + """Tests whether deserialising completes the missing containers with empty ones. The initial containers are just the + + definition and the definition_changes (that cannot be empty after CURA-5281) + """ + global_stack._containers = [DefinitionContainer(container_id = "definition"), global_stack.definitionChanges] #Set the internal state of this stack manually. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -229,8 +236,9 @@ def test_deserializeCompletesEmptyContainers(global_stack): assert global_stack.getContainer(container_type_index) == empty_container #All others need to be empty. -## Tests whether an instance container with the wrong type gets removed when deserialising. def test_deserializeRemovesWrongInstanceContainer(global_stack): + """Tests whether an instance container with the wrong type gets removed when deserialising.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "wrong type") global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -240,8 +248,9 @@ def test_deserializeRemovesWrongInstanceContainer(global_stack): assert global_stack.quality == global_stack._empty_instance_container #Replaced with empty. -## Tests whether a container with the wrong class gets removed when deserialising. def test_deserializeRemovesWrongContainerClass(global_stack): + """Tests whether a container with the wrong class gets removed when deserialising.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = DefinitionContainer(container_id = "wrong class") global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -251,8 +260,9 @@ def test_deserializeRemovesWrongContainerClass(global_stack): assert global_stack.quality == global_stack._empty_instance_container #Replaced with empty. -## Tests whether an instance container in the definition spot results in an error. def test_deserializeWrongDefinitionClass(global_stack): + """Tests whether an instance container in the definition spot results in an error.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = getInstanceContainer(container_type = "definition") #Correct type but wrong class. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -260,8 +270,9 @@ def test_deserializeWrongDefinitionClass(global_stack): global_stack.deserialize("") -## Tests whether an instance container with the wrong type is moved into the correct slot by deserialising. def test_deserializeMoveInstanceContainer(global_stack): + """Tests whether an instance container with the wrong type is moved into the correct slot by deserialising.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "material") #Not in the correct spot. global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -272,8 +283,9 @@ def test_deserializeMoveInstanceContainer(global_stack): assert global_stack.material != empty_container -## Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising. def test_deserializeMoveDefinitionContainer(global_stack): + """Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Material] = DefinitionContainer(container_id = "some definition") #Not in the correct spot. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -283,8 +295,9 @@ def test_deserializeMoveDefinitionContainer(global_stack): assert global_stack.definition != empty_container -## Tests whether getProperty properly applies the stack-like behaviour on its containers. def test_getPropertyFallThrough(global_stack): + """Tests whether getProperty properly applies the stack-like behaviour on its containers.""" + #A few instance container mocks to put in the stack. mock_layer_heights = {} #For each container type, a mock container that defines layer height to something unique. mock_no_settings = {} #For each container type, a mock container that has no settings at all. @@ -326,8 +339,9 @@ def test_getPropertyFallThrough(global_stack): assert global_stack.getProperty("layer_height", "value") == container_indexes.UserChanges -## In definitions, test whether having no resolve allows us to find the value. def test_getPropertyNoResolveInDefinition(global_stack): + """In definitions, test whether having no resolve allows us to find the value.""" + value = unittest.mock.MagicMock() #Just sets the value for bed temperature. value.getProperty = lambda key, property, context = None: 10 if (key == "material_bed_temperature" and property == "value") else None @@ -336,8 +350,9 @@ def test_getPropertyNoResolveInDefinition(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 10 #No resolve, so fall through to value. -## In definitions, when the value is asked and there is a resolve function, it must get the resolve first. def test_getPropertyResolveInDefinition(global_stack): + """In definitions, when the value is asked and there is a resolve function, it must get the resolve first.""" + resolve_and_value = unittest.mock.MagicMock() #Sets the resolve and value for bed temperature. resolve_and_value.getProperty = lambda key, property, context = None: (7.5 if property == "resolve" else 5) if (key == "material_bed_temperature" and property in ("resolve", "value")) else None #7.5 resolve, 5 value. @@ -346,8 +361,9 @@ def test_getPropertyResolveInDefinition(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 7.5 #Resolve wins in the definition. -## In instance containers, when the value is asked and there is a resolve function, it must get the value first. def test_getPropertyResolveInInstance(global_stack): + """In instance containers, when the value is asked and there is a resolve function, it must get the value first.""" + container_indices = cura.Settings.CuraContainerStack._ContainerIndexes instance_containers = {} for container_type in container_indices.IndexTypeMap: @@ -373,8 +389,9 @@ def test_getPropertyResolveInInstance(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 5 -## Tests whether the value in instances gets evaluated before the resolve in definitions. def test_getPropertyInstancesBeforeResolve(global_stack): + """Tests whether the value in instances gets evaluated before the resolve in definitions.""" + def getValueProperty(key, property, context = None): if key != "material_bed_temperature": return None @@ -404,8 +421,9 @@ def test_getPropertyInstancesBeforeResolve(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 10 -## Tests whether the hasUserValue returns true for settings that are changed in the user-changes container. def test_hasUserValueUserChanges(global_stack): + """Tests whether the hasUserValue returns true for settings that are changed in the user-changes container.""" + container = unittest.mock.MagicMock() container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "user") container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set. @@ -416,8 +434,9 @@ def test_hasUserValueUserChanges(global_stack): assert not global_stack.hasUserValue("") -## Tests whether the hasUserValue returns true for settings that are changed in the quality-changes container. def test_hasUserValueQualityChanges(global_stack): + """Tests whether the hasUserValue returns true for settings that are changed in the quality-changes container.""" + container = unittest.mock.MagicMock() container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "quality_changes") container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set. @@ -428,8 +447,9 @@ def test_hasUserValueQualityChanges(global_stack): assert not global_stack.hasUserValue("") -## Tests whether a container in some other place on the stack is correctly not recognised as user value. def test_hasNoUserValue(global_stack): + """Tests whether a container in some other place on the stack is correctly not recognised as user value.""" + container = unittest.mock.MagicMock() container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "quality") container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set. @@ -438,20 +458,23 @@ def test_hasNoUserValue(global_stack): assert not global_stack.hasUserValue("layer_height") #However this container is quality, so it's not a user value. -## Tests whether inserting a container is properly forbidden. def test_insertContainer(global_stack): + """Tests whether inserting a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): global_stack.insertContainer(0, unittest.mock.MagicMock()) -## Tests whether removing a container is properly forbidden. def test_removeContainer(global_stack): + """Tests whether removing a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): global_stack.removeContainer(unittest.mock.MagicMock()) -## Tests whether changing the next stack is properly forbidden. def test_setNextStack(global_stack): + """Tests whether changing the next stack is properly forbidden.""" + with pytest.raises(InvalidOperationError): global_stack.setNextStack(unittest.mock.MagicMock()) diff --git a/tests/Settings/TestProfiles.py b/tests/Settings/TestProfiles.py index cf26ad7020..fba57c5eea 100644 --- a/tests/Settings/TestProfiles.py +++ b/tests/Settings/TestProfiles.py @@ -61,9 +61,10 @@ variant_filepaths = collectAllVariants() intent_filepaths = collectAllIntents() -## Attempt to load all the quality profiles. @pytest.mark.parametrize("file_name", quality_filepaths) def test_validateQualityProfiles(file_name): + """Attempt to load all the quality profiles.""" + try: with open(file_name, encoding = "utf-8") as data: serialized = data.read() @@ -114,9 +115,10 @@ def test_validateIntentProfiles(file_name): # File can't be read, header sections missing, whatever the case, this shouldn't happen! assert False, "Got an exception while reading the file {file_name}: {err}".format(file_name = file_name, err = str(e)) -## Attempt to load all the variant profiles. @pytest.mark.parametrize("file_name", variant_filepaths) def test_validateVariantProfiles(file_name): + """Attempt to load all the variant profiles.""" + try: with open(file_name, encoding = "utf-8") as data: serialized = data.read() diff --git a/tests/Settings/TestSettingOverrideDecorator.py b/tests/Settings/TestSettingOverrideDecorator.py index 50c23c409f..4976ce81a7 100644 --- a/tests/Settings/TestSettingOverrideDecorator.py +++ b/tests/Settings/TestSettingOverrideDecorator.py @@ -24,6 +24,11 @@ def setting_override_decorator(): def test_onSettingValueChanged(setting_override_decorator): + def mock_getRawProperty(key, property_name, *args, **kwargs): + if property_name == "limit_to_extruder": + return "-1" + return MagicMock(name="rawProperty") + container_registry.findContainerStacks().__getitem__().getRawProperty = mock_getRawProperty # On creation the needs slicing should be called once (as it being added should trigger a reslice) assert application.getBackend().needsSlicing.call_count == 1 with patch("UM.Application.Application.getInstance", MagicMock(return_value=application)): diff --git a/tests/TestArrange.py b/tests/TestArrange.py index a00b544936..f37e48f19c 100755 --- a/tests/TestArrange.py +++ b/tests/TestArrange.py @@ -6,36 +6,43 @@ import numpy from cura.Arranging.Arrange import Arrange from cura.Arranging.ShapeArray import ShapeArray -## Triangle of area 12 def gimmeTriangle(): + """Triangle of area 12""" + return numpy.array([[-3, 1], [3, 1], [0, -3]], dtype=numpy.int32) -## Boring square def gimmeSquare(): + """Boring square""" + return numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32) -## Triangle of area 12 def gimmeShapeArray(scale = 1.0): + """Triangle of area 12""" + vertices = gimmeTriangle() shape_arr = ShapeArray.fromPolygon(vertices, scale = scale) return shape_arr -## Boring square def gimmeShapeArraySquare(scale = 1.0): + """Boring square""" + vertices = gimmeSquare() shape_arr = ShapeArray.fromPolygon(vertices, scale = scale) return shape_arr -## Smoke test for Arrange def test_smoke_arrange(): + """Smoke test for Arrange""" + Arrange.create(fixed_nodes = []) -## Smoke test for ShapeArray def test_smoke_ShapeArray(): + """Smoke test for ShapeArray""" + gimmeShapeArray() -## Test ShapeArray def test_ShapeArray(): + """Test ShapeArray""" + scale = 1 ar = Arrange(16, 16, 8, 8, scale = scale) ar.centerFirst() @@ -44,8 +51,9 @@ def test_ShapeArray(): count = len(numpy.where(shape_arr.arr == 1)[0]) assert count >= 10 # should approach 12 -## Test ShapeArray with scaling def test_ShapeArray_scaling(): + """Test ShapeArray with scaling""" + scale = 2 ar = Arrange(16, 16, 8, 8, scale = scale) ar.centerFirst() @@ -54,8 +62,9 @@ def test_ShapeArray_scaling(): count = len(numpy.where(shape_arr.arr == 1)[0]) assert count >= 40 # should approach 2*2*12 = 48 -## Test ShapeArray with scaling def test_ShapeArray_scaling2(): + """Test ShapeArray with scaling""" + scale = 0.5 ar = Arrange(16, 16, 8, 8, scale = scale) ar.centerFirst() @@ -64,8 +73,9 @@ def test_ShapeArray_scaling2(): count = len(numpy.where(shape_arr.arr == 1)[0]) assert count >= 1 # should approach 3, but it can be inaccurate due to pixel rounding -## Test centerFirst def test_centerFirst(): + """Test centerFirst""" + ar = Arrange(300, 300, 150, 150, scale = 1) ar.centerFirst() assert ar._priority[150][150] < ar._priority[170][150] @@ -75,8 +85,9 @@ def test_centerFirst(): assert ar._priority[150][150] < ar._priority[150][130] assert ar._priority[150][150] < ar._priority[130][130] -## Test centerFirst def test_centerFirst_rectangular(): + """Test centerFirst""" + ar = Arrange(400, 300, 200, 150, scale = 1) ar.centerFirst() assert ar._priority[150][200] < ar._priority[150][220] @@ -86,15 +97,17 @@ def test_centerFirst_rectangular(): assert ar._priority[150][200] < ar._priority[130][200] assert ar._priority[150][200] < ar._priority[130][180] -## Test centerFirst def test_centerFirst_rectangular2(): + """Test centerFirst""" + ar = Arrange(10, 20, 5, 10, scale = 1) ar.centerFirst() assert ar._priority[10][5] < ar._priority[10][7] -## Test backFirst def test_backFirst(): + """Test backFirst""" + ar = Arrange(300, 300, 150, 150, scale = 1) ar.backFirst() assert ar._priority[150][150] < ar._priority[170][150] @@ -102,8 +115,9 @@ def test_backFirst(): assert ar._priority[150][150] > ar._priority[130][150] assert ar._priority[150][150] > ar._priority[130][130] -## See if the result of bestSpot has the correct form def test_smoke_bestSpot(): + """See if the result of bestSpot has the correct form""" + ar = Arrange(30, 30, 15, 15, scale = 1) ar.centerFirst() @@ -114,8 +128,9 @@ def test_smoke_bestSpot(): assert hasattr(best_spot, "penalty_points") assert hasattr(best_spot, "priority") -## Real life test def test_bestSpot(): + """Real life test""" + ar = Arrange(16, 16, 8, 8, scale = 1) ar.centerFirst() @@ -131,8 +146,9 @@ def test_bestSpot(): assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location ar.place(best_spot.x, best_spot.y, shape_arr) -## Real life test rectangular build plate def test_bestSpot_rectangular_build_plate(): + """Real life test rectangular build plate""" + ar = Arrange(16, 40, 8, 20, scale = 1) ar.centerFirst() @@ -164,8 +180,9 @@ def test_bestSpot_rectangular_build_plate(): best_spot_x = ar.bestSpot(shape_arr) ar.place(best_spot_x.x, best_spot_x.y, shape_arr) -## Real life test def test_bestSpot_scale(): + """Real life test""" + scale = 0.5 ar = Arrange(16, 16, 8, 8, scale = scale) ar.centerFirst() @@ -182,8 +199,9 @@ def test_bestSpot_scale(): assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location ar.place(best_spot.x, best_spot.y, shape_arr) -## Real life test def test_bestSpot_scale_rectangular(): + """Real life test""" + scale = 0.5 ar = Arrange(16, 40, 8, 20, scale = scale) ar.centerFirst() @@ -205,8 +223,9 @@ def test_bestSpot_scale_rectangular(): best_spot = ar.bestSpot(shape_arr_square) ar.place(best_spot.x, best_spot.y, shape_arr_square) -## Try to place an object and see if something explodes def test_smoke_place(): + """Try to place an object and see if something explodes""" + ar = Arrange(30, 30, 15, 15) ar.centerFirst() @@ -216,8 +235,9 @@ def test_smoke_place(): ar.place(0, 0, shape_arr) assert numpy.any(ar._occupied) -## See of our center has less penalty points than out of the center def test_checkShape(): + """See of our center has less penalty points than out of the center""" + ar = Arrange(30, 30, 15, 15) ar.centerFirst() @@ -228,8 +248,9 @@ def test_checkShape(): assert points2 > points assert points3 > points -## See of our center has less penalty points than out of the center def test_checkShape_rectangular(): + """See of our center has less penalty points than out of the center""" + ar = Arrange(20, 30, 10, 15) ar.centerFirst() @@ -240,8 +261,9 @@ def test_checkShape_rectangular(): assert points2 > points assert points3 > points -## Check that placing an object on occupied place returns None. def test_checkShape_place(): + """Check that placing an object on occupied place returns None.""" + ar = Arrange(30, 30, 15, 15) ar.centerFirst() @@ -252,8 +274,9 @@ def test_checkShape_place(): assert points2 is None -## Test the whole sequence def test_smoke_place_objects(): + """Test the whole sequence""" + ar = Arrange(20, 20, 10, 10, scale = 1) ar.centerFirst() shape_arr = gimmeShapeArray() @@ -268,26 +291,30 @@ def test_compare_occupied_and_priority_tables(): ar.centerFirst() assert ar._priority.shape == ar._occupied.shape -## Polygon -> array def test_arrayFromPolygon(): + """Polygon -> array""" + vertices = numpy.array([[-3, 1], [3, 1], [0, -3]]) array = ShapeArray.arrayFromPolygon([5, 5], vertices) assert numpy.any(array) -## Polygon -> array def test_arrayFromPolygon2(): + """Polygon -> array""" + vertices = numpy.array([[-3, 1], [3, 1], [2, -3]]) array = ShapeArray.arrayFromPolygon([5, 5], vertices) assert numpy.any(array) -## Polygon -> array def test_fromPolygon(): + """Polygon -> array""" + vertices = numpy.array([[0, 0.5], [0, 0], [0.5, 0]]) array = ShapeArray.fromPolygon(vertices, scale=0.5) assert numpy.any(array.arr) -## Line definition -> array with true/false def test_check(): + """Line definition -> array with true/false""" + base_array = numpy.zeros([5, 5], dtype=float) p1 = numpy.array([0, 0]) p2 = numpy.array([4, 4]) @@ -296,8 +323,9 @@ def test_check(): assert check_array[3][0] assert not check_array[0][3] -## Line definition -> array with true/false def test_check2(): + """Line definition -> array with true/false""" + base_array = numpy.zeros([5, 5], dtype=float) p1 = numpy.array([0, 3]) p2 = numpy.array([4, 3]) @@ -306,8 +334,9 @@ def test_check2(): assert not check_array[3][0] assert check_array[3][4] -## Just adding some stuff to ensure fromNode works as expected. Some parts should actually be in UM def test_parts_of_fromNode(): + """Just adding some stuff to ensure fromNode works as expected. Some parts should actually be in UM""" + from UM.Math.Polygon import Polygon p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32)) offset = 1 diff --git a/tests/TestBuildVolume.py b/tests/TestBuildVolume.py index 11de412592..fc49483e5a 100644 --- a/tests/TestBuildVolume.py +++ b/tests/TestBuildVolume.py @@ -1,3 +1,6 @@ +# Copyright (c) 2020 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + from unittest.mock import MagicMock, patch import pytest @@ -6,9 +9,6 @@ from UM.Math.Vector import Vector from cura.BuildVolume import BuildVolume, PRIME_CLEARANCE import numpy - - - @pytest.fixture def build_volume() -> BuildVolume: mocked_application = MagicMock() @@ -161,12 +161,10 @@ class TestUpdateRaftThickness: return properties.get(args[2]) def createMockedStack(self): - mocked_global_stack = MagicMock(name="mocked_global_stack") - mocked_global_stack.getProperty = MagicMock(side_effect=self.getPropertySideEffect) + mocked_global_stack = MagicMock(name = "mocked_global_stack") + mocked_global_stack.getProperty = MagicMock(side_effect = self.getPropertySideEffect) extruder_stack = MagicMock() - mocked_global_stack.extruders = {"0": extruder_stack} - return mocked_global_stack def test_simple(self, build_volume: BuildVolume): diff --git a/tests/TestExtruderManager.py b/tests/TestExtruderManager.py index f732278e83..6eca0a34ad 100644 --- a/tests/TestExtruderManager.py +++ b/tests/TestExtruderManager.py @@ -15,17 +15,3 @@ def test_getAllExtruderSettings(extruder_manager): extruder_2.getProperty = MagicMock(return_value="zomg") extruder_manager.getActiveExtruderStacks = MagicMock(return_value = [extruder_1, extruder_2]) assert extruder_manager.getAllExtruderSettings("whatever", "value") == ["beep", "zomg"] - - -def test_registerExtruder(extruder_manager): - extruder = createMockedExtruder("beep") - extruder.getMetaDataEntry = MagicMock(return_value = "0") # because the extruder position gets called - - extruder_manager.extrudersChanged = MagicMock() - extruder_manager.registerExtruder(extruder, "zomg") - - assert extruder_manager.extrudersChanged.emit.call_count == 1 - - # Doing it again should not trigger anything - extruder_manager.registerExtruder(extruder, "zomg") - assert extruder_manager.extrudersChanged.emit.call_count == 1 diff --git a/tests/TestPrintInformation.py b/tests/TestPrintInformation.py index 27934a91d8..5133dfcafb 100644 --- a/tests/TestPrintInformation.py +++ b/tests/TestPrintInformation.py @@ -22,7 +22,6 @@ def getPrintInformation(printer_name) -> PrintInformation: mocked_preferences.getValue = MagicMock(return_value = '{"omgzomg": {"spool_weight": 10, "spool_cost": 9}}') global_container_stack = MagicMock() - global_container_stack.extruders = {"0": mocked_extruder_stack} global_container_stack.definition.getName = MagicMock(return_value = printer_name) mock_application.getGlobalContainerStack = MagicMock(return_value = global_container_stack) mock_application.getPreferences = MagicMock(return_value = mocked_preferences)