mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-10 23:35:07 -06:00
Merge remote-tracking branch 'origin/master' into WIP_kill_extruder_manager
This commit is contained in:
commit
18cb21186c
129 changed files with 3708 additions and 2220 deletions
117
cura/API/Account.py
Normal file
117
cura/API/Account.py
Normal file
|
@ -0,0 +1,117 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, Dict, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Message import Message
|
||||
|
||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||
from cura.OAuth2.Models import OAuth2Settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## 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):
|
||||
# Signal emitted when user logged in or out.
|
||||
loginStateChanged = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._application = application
|
||||
|
||||
self._error_message = None # type: Optional[Message]
|
||||
self._logged_in = False
|
||||
|
||||
self._callback_port = 32118
|
||||
self._oauth_root = "https://account.ultimaker.com"
|
||||
self._cloud_api_root = "https://api.ultimaker.com"
|
||||
|
||||
self._oauth_settings = OAuth2Settings(
|
||||
OAUTH_SERVER_URL= self._oauth_root,
|
||||
CALLBACK_PORT=self._callback_port,
|
||||
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
||||
CLIENT_ID="um---------------ultimaker_cura_drive_plugin",
|
||||
CLIENT_SCOPES="user.read drive.backups.read drive.backups.write",
|
||||
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
|
||||
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
|
||||
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
|
||||
)
|
||||
|
||||
self._authorization_service = AuthorizationService(self._oauth_settings)
|
||||
|
||||
def initialize(self) -> None:
|
||||
self._authorization_service.initialize(self._application.getPreferences())
|
||||
|
||||
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
|
||||
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
|
||||
self._authorization_service.loadAuthDataFromPreferences()
|
||||
|
||||
@pyqtProperty(bool, notify=loginStateChanged)
|
||||
def isLoggedIn(self) -> bool:
|
||||
return self._logged_in
|
||||
|
||||
def _onLoginStateChanged(self, logged_in: bool = False, error_message: Optional[str] = None) -> None:
|
||||
if error_message:
|
||||
if self._error_message:
|
||||
self._error_message.hide()
|
||||
self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
|
||||
self._error_message.show()
|
||||
|
||||
if self._logged_in != logged_in:
|
||||
self._logged_in = logged_in
|
||||
self.loginStateChanged.emit(logged_in)
|
||||
|
||||
@pyqtSlot()
|
||||
def login(self) -> None:
|
||||
if self._logged_in:
|
||||
# Nothing to do, user already logged in.
|
||||
return
|
||||
self._authorization_service.startAuthorizationFlow()
|
||||
|
||||
@pyqtProperty(str, notify=loginStateChanged)
|
||||
def userName(self):
|
||||
user_profile = self._authorization_service.getUserProfile()
|
||||
if not user_profile:
|
||||
return None
|
||||
return user_profile.username
|
||||
|
||||
@pyqtProperty(str, notify = loginStateChanged)
|
||||
def profileImageUrl(self):
|
||||
user_profile = self._authorization_service.getUserProfile()
|
||||
if not user_profile:
|
||||
return None
|
||||
return user_profile.profile_image_url
|
||||
|
||||
@pyqtProperty(str, notify=loginStateChanged)
|
||||
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]]]:
|
||||
user_profile = self._authorization_service.getUserProfile()
|
||||
if not user_profile:
|
||||
return None
|
||||
return user_profile.__dict__
|
||||
|
||||
@pyqtSlot()
|
||||
def logout(self) -> None:
|
||||
if not self._logged_in:
|
||||
return # Nothing to do, user isn't logged in.
|
||||
|
||||
self._authorization_service.deleteAuthData()
|
|
@ -1,9 +1,12 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Tuple, Optional
|
||||
from typing import Tuple, Optional, TYPE_CHECKING
|
||||
|
||||
from cura.Backups.BackupsManager import BackupsManager
|
||||
|
||||
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.
|
||||
|
@ -13,9 +16,10 @@ from cura.Backups.BackupsManager import BackupsManager
|
|||
# api = CuraAPI()
|
||||
# api.backups.createBackup()
|
||||
# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
|
||||
|
||||
class Backups:
|
||||
manager = BackupsManager() # Re-used instance of the backups manager.
|
||||
|
||||
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
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
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.
|
||||
|
@ -19,8 +23,9 @@ from cura.CuraApplication import CuraApplication
|
|||
# api.interface.settings.addContextMenuItem(data)``
|
||||
|
||||
class Settings:
|
||||
# Re-used instance of Cura:
|
||||
application = CuraApplication.getInstance() # type: CuraApplication
|
||||
|
||||
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.
|
||||
|
@ -30,4 +35,4 @@ class Settings:
|
|||
## Get all custom items currently added to the sidebar context menu.
|
||||
# \return List containing all custom context menu items.
|
||||
def getContextMenuItems(self) -> list:
|
||||
return self.application.getSidebarCustomMenuItems()
|
||||
return self.application.getSidebarCustomMenuItems()
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.API.Interface.Settings import Settings
|
||||
|
||||
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.
|
||||
#
|
||||
|
@ -20,5 +26,6 @@ class Interface:
|
|||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
|
||||
# API methods specific to the settings portion of the UI
|
||||
settings = Settings()
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
# API methods specific to the settings portion of the UI
|
||||
self.settings = Settings(application)
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtProperty
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.API.Backups import Backups
|
||||
from cura.API.Interface import Interface
|
||||
from cura.API.Account import Account
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## The official Cura API that plug-ins can use to interact with Cura.
|
||||
#
|
||||
|
@ -10,14 +19,47 @@ from cura.API.Interface import Interface
|
|||
# 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:
|
||||
class CuraAPI(QObject):
|
||||
|
||||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
__instance = None # type: "CuraAPI"
|
||||
_application = None # type: CuraApplication
|
||||
|
||||
# Backups API
|
||||
backups = Backups()
|
||||
# This is done to ensure that the first time an instance is created, it's forced that the application is set.
|
||||
# The main reason for this is that we want to prevent consumers of API to have a dependency on CuraApplication.
|
||||
# Since the API is intended to be used by plugins, the cura application should have already created this.
|
||||
def __new__(cls, application: Optional["CuraApplication"] = None):
|
||||
if cls.__instance is None:
|
||||
if application is None:
|
||||
raise Exception("Upon first time creation, the application must be set.")
|
||||
cls.__instance = super(CuraAPI, cls).__new__(cls)
|
||||
cls._application = application
|
||||
return cls.__instance
|
||||
|
||||
# Interface API
|
||||
interface = Interface()
|
||||
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)
|
||||
|
||||
# Interface API
|
||||
self._interface = Interface(self._application)
|
||||
|
||||
def initialize(self) -> None:
|
||||
self._account.initialize()
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def account(self) -> "Account":
|
||||
return self._account
|
||||
|
||||
@property
|
||||
def backups(self) -> "Backups":
|
||||
return self._backups
|
||||
|
||||
@property
|
||||
def interface(self) -> "Interface":
|
||||
return self._interface
|
|
@ -4,18 +4,18 @@
|
|||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
import shutil
|
||||
|
||||
from typing import Dict, Optional
|
||||
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
|
||||
from UM import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Platform import Platform
|
||||
from UM.Resources import Resources
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## The back-up class holds all data about a back-up.
|
||||
|
@ -29,24 +29,25 @@ class Backup:
|
|||
# Re-use translation catalog.
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def __init__(self, zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None:
|
||||
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:
|
||||
cura_release = CuraApplication.getInstance().getVersion()
|
||||
cura_release = self._application.getVersion()
|
||||
version_data_dir = Resources.getDataStoragePath()
|
||||
|
||||
Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
|
||||
|
||||
# Ensure all current settings are saved.
|
||||
CuraApplication.getInstance().saveSettings()
|
||||
self._application.saveSettings()
|
||||
|
||||
# We copy the preferences file to the user data directory in Linux as it's in a different location there.
|
||||
# When restoring a backup on Linux, we move it back.
|
||||
if Platform.isLinux():
|
||||
preferences_file_name = CuraApplication.getInstance().getApplicationName()
|
||||
preferences_file_name = self._application.getApplicationName()
|
||||
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
||||
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
||||
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
|
||||
|
@ -58,7 +59,7 @@ class Backup:
|
|||
if archive is None:
|
||||
return
|
||||
files = archive.namelist()
|
||||
|
||||
|
||||
# Count the metadata items. We do this in a rather naive way at the moment.
|
||||
machine_count = len([s for s in files if "machine_instances/" in s]) - 1
|
||||
material_count = len([s for s in files if "materials/" in s]) - 1
|
||||
|
@ -112,7 +113,7 @@ class Backup:
|
|||
"Tried to restore a Cura backup without having proper data or meta data."))
|
||||
return False
|
||||
|
||||
current_version = CuraApplication.getInstance().getVersion()
|
||||
current_version = self._application.getVersion()
|
||||
version_to_restore = self.meta_data.get("cura_release", "master")
|
||||
if current_version != version_to_restore:
|
||||
# Cannot restore version older or newer than current because settings might have changed.
|
||||
|
@ -128,7 +129,7 @@ class Backup:
|
|||
|
||||
# Under Linux, preferences are stored elsewhere, so we copy the file to there.
|
||||
if Platform.isLinux():
|
||||
preferences_file_name = CuraApplication.getInstance().getApplicationName()
|
||||
preferences_file_name = self._application.getApplicationName()
|
||||
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
||||
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
||||
Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Dict, Optional, Tuple
|
||||
from typing import Dict, Optional, Tuple, TYPE_CHECKING
|
||||
|
||||
from UM.Logger import Logger
|
||||
from cura.Backups.Backup import Backup
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## The BackupsManager is responsible for managing the creating and restoring of
|
||||
|
@ -13,15 +15,15 @@ from cura.CuraApplication import CuraApplication
|
|||
#
|
||||
# Back-ups themselves are represented in a different class.
|
||||
class BackupsManager:
|
||||
def __init__(self):
|
||||
self._application = CuraApplication.getInstance()
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
self._application = application
|
||||
|
||||
## 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).
|
||||
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
|
||||
self._disableAutoSave()
|
||||
backup = Backup()
|
||||
backup = Backup(self._application)
|
||||
backup.makeFromCurrent()
|
||||
self._enableAutoSave()
|
||||
# We don't return a Backup here because we want plugins only to interact with our API and not full objects.
|
||||
|
@ -39,7 +41,7 @@ class BackupsManager:
|
|||
|
||||
self._disableAutoSave()
|
||||
|
||||
backup = Backup(zip_file = zip_file, meta_data = meta_data)
|
||||
backup = Backup(self._application, zip_file = zip_file, meta_data = meta_data)
|
||||
restored = backup.restore()
|
||||
if restored:
|
||||
# At this point, Cura will need to restart for the changes to take effect.
|
||||
|
|
|
@ -479,8 +479,6 @@ class BuildVolume(SceneNode):
|
|||
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
|
||||
)
|
||||
|
||||
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
|
||||
|
||||
self.updateNodeBoundaryCheck()
|
||||
|
||||
def getBoundingBox(self) -> AxisAlignedBox:
|
||||
|
|
|
@ -44,6 +44,7 @@ from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
|||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||
|
||||
from cura.API import CuraAPI
|
||||
from cura.Arranging.Arrange import Arrange
|
||||
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||
|
@ -61,6 +62,7 @@ from cura.Scene.CuraSceneController import CuraSceneController
|
|||
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
from cura.Settings.MachineNameValidator import MachineNameValidator
|
||||
|
||||
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
||||
|
@ -206,6 +208,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
self._quality_profile_drop_down_menu_model = None
|
||||
self._custom_quality_profile_drop_down_menu_model = None
|
||||
self._cura_API = CuraAPI(self)
|
||||
|
||||
self._physics = None
|
||||
self._volume = None
|
||||
|
@ -244,6 +247,8 @@ class CuraApplication(QtApplication):
|
|||
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
self._container_registry_class = CuraContainerRegistry
|
||||
# Redefined here in order to please the typing.
|
||||
self._container_registry = None # type: CuraContainerRegistry
|
||||
from cura.CuraPackageManager import CuraPackageManager
|
||||
self._package_manager_class = CuraPackageManager
|
||||
|
||||
|
@ -268,6 +273,9 @@ class CuraApplication(QtApplication):
|
|||
help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog.")
|
||||
self._cli_parser.add_argument("file", nargs = "*", help = "Files to load after starting the application.")
|
||||
|
||||
def getContainerRegistry(self) -> "CuraContainerRegistry":
|
||||
return self._container_registry
|
||||
|
||||
def parseCliOptions(self):
|
||||
super().parseCliOptions()
|
||||
|
||||
|
@ -679,7 +687,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
Logger.log("i", "Initializing quality manager")
|
||||
from cura.Machines.QualityManager import QualityManager
|
||||
self._quality_manager = QualityManager(container_registry, parent = self)
|
||||
self._quality_manager = QualityManager(self, parent = self)
|
||||
self._quality_manager.initialize()
|
||||
|
||||
Logger.log("i", "Initializing machine manager")
|
||||
|
@ -711,6 +719,9 @@ class CuraApplication(QtApplication):
|
|||
default_visibility_profile = self._setting_visibility_presets_model.getItem(0)
|
||||
self.getPreferences().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
|
||||
|
||||
# Initialize Cura API
|
||||
self._cura_API.initialize()
|
||||
|
||||
# Detect in which mode to run and execute that mode
|
||||
if self._is_headless:
|
||||
self.runWithoutGUI()
|
||||
|
@ -903,6 +914,9 @@ class CuraApplication(QtApplication):
|
|||
self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self)
|
||||
return self._custom_quality_profile_drop_down_menu_model
|
||||
|
||||
def getCuraAPI(self, *args, **kwargs) -> "CuraAPI":
|
||||
return self._cura_API
|
||||
|
||||
## Registers objects for the QML engine to use.
|
||||
#
|
||||
# \param engine The QML engine.
|
||||
|
@ -951,6 +965,9 @@ class CuraApplication(QtApplication):
|
|||
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
|
||||
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
|
||||
|
||||
from cura.API import CuraAPI
|
||||
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
|
||||
|
||||
# As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
|
||||
actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
|
||||
qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions")
|
||||
|
@ -1590,6 +1607,11 @@ class CuraApplication(QtApplication):
|
|||
job.start()
|
||||
|
||||
def _readMeshFinished(self, job):
|
||||
global_container_stack = self.getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
Logger.log("w", "Can't load meshes before a printer is added.")
|
||||
return
|
||||
|
||||
nodes = job.getResult()
|
||||
file_name = job.getFileName()
|
||||
file_name_lower = file_name.lower()
|
||||
|
@ -1604,7 +1626,6 @@ class CuraApplication(QtApplication):
|
|||
for node_ in DepthFirstIterator(root):
|
||||
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
|
||||
fixed_nodes.append(node_)
|
||||
global_container_stack = self.getGlobalContainerStack()
|
||||
machine_width = global_container_stack.getProperty("machine_width", "value")
|
||||
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
||||
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes)
|
||||
|
|
|
@ -9,9 +9,6 @@ from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
|||
from UM.Logger import Logger
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Machines.QualityGroup import QualityGroup
|
||||
|
||||
|
||||
##
|
||||
# A metadata / container combination. Use getContainer() to get the container corresponding to the metadata.
|
||||
|
@ -24,11 +21,11 @@ if TYPE_CHECKING:
|
|||
# This is used in Variant, Material, and Quality Managers.
|
||||
#
|
||||
class ContainerNode:
|
||||
__slots__ = ("_metadata", "container", "children_map")
|
||||
__slots__ = ("_metadata", "_container", "children_map")
|
||||
|
||||
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
self._metadata = metadata
|
||||
self.container = None
|
||||
self._container = None # type: Optional[InstanceContainer]
|
||||
self.children_map = OrderedDict() # type: ignore # This is because it's children are supposed to override it.
|
||||
|
||||
## Get an entry value from the metadata
|
||||
|
@ -50,7 +47,7 @@ class ContainerNode:
|
|||
Logger.log("e", "Cannot get container for a ContainerNode without metadata.")
|
||||
return None
|
||||
|
||||
if self.container is None:
|
||||
if self._container is None:
|
||||
container_id = self._metadata["id"]
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
|
||||
|
@ -59,9 +56,9 @@ class ContainerNode:
|
|||
error_message = ConfigurationErrorMessage.getInstance()
|
||||
error_message.addFaultyContainers(container_id)
|
||||
return None
|
||||
self.container = container_list[0]
|
||||
self._container = container_list[0]
|
||||
|
||||
return self.container
|
||||
return self._container
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%s[%s]" % (self.__class__.__name__, self.getMetaDataEntry("id"))
|
||||
|
|
|
@ -21,7 +21,6 @@ from .VariantType import VariantType
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
|
||||
|
@ -352,7 +351,8 @@ class MaterialManager(QObject):
|
|||
if material_id not in material_id_metadata_dict:
|
||||
material_id_metadata_dict[material_id] = node
|
||||
|
||||
Logger.log("d", "Exclude materials {excluded_materials} for machine {machine_definition_id}".format(excluded_materials = ", ".join(excluded_materials), machine_definition_id = machine_definition_id))
|
||||
if excluded_materials:
|
||||
Logger.log("d", "Exclude materials {excluded_materials} for machine {machine_definition_id}".format(excluded_materials = ", ".join(excluded_materials), machine_definition_id = machine_definition_id))
|
||||
|
||||
return material_id_metadata_dict
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional
|
||||
from typing import Optional, List, Dict, Union
|
||||
import os
|
||||
import urllib.parse
|
||||
from configparser import ConfigParser
|
||||
|
@ -58,9 +58,9 @@ class SettingVisibilityPresetsModel(ListModel):
|
|||
break
|
||||
return result
|
||||
|
||||
def _populate(self):
|
||||
def _populate(self) -> None:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
items = []
|
||||
items = [] # type: List[Dict[str, Union[str, int, List[str]]]]
|
||||
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
||||
try:
|
||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
|
||||
|
@ -79,7 +79,7 @@ class SettingVisibilityPresetsModel(ListModel):
|
|||
if not parser.has_option("general", "name") or not parser.has_option("general", "weight"):
|
||||
continue
|
||||
|
||||
settings = []
|
||||
settings = [] # type: List[str]
|
||||
for section in parser.sections():
|
||||
if section == 'general':
|
||||
continue
|
||||
|
@ -98,7 +98,7 @@ class SettingVisibilityPresetsModel(ListModel):
|
|||
except Exception:
|
||||
Logger.logException("e", "Failed to load setting preset %s", file_path)
|
||||
|
||||
items.sort(key = lambda k: (int(k["weight"]), k["id"]))
|
||||
items.sort(key = lambda k: (int(k["weight"]), k["id"])) # type: ignore
|
||||
# Put "custom" at the top
|
||||
items.insert(0, {"id": "custom",
|
||||
"name": "Custom selection",
|
||||
|
@ -147,7 +147,7 @@ class SettingVisibilityPresetsModel(ListModel):
|
|||
def activePreset(self) -> str:
|
||||
return self._active_preset_item["id"]
|
||||
|
||||
def _onPreferencesChanged(self, name: str):
|
||||
def _onPreferencesChanged(self, name: str) -> None:
|
||||
if name != "general/visible_settings":
|
||||
return
|
||||
|
||||
|
|
|
@ -24,9 +24,9 @@ class QualityChangesGroup(QualityGroup):
|
|||
ConfigurationErrorMessage.getInstance().addFaultyContainers(node.getMetaDataEntry("id"))
|
||||
return
|
||||
|
||||
if extruder_position is None: #Then we're a global quality changes profile.
|
||||
if extruder_position is None: # Then we're a global quality changes profile.
|
||||
self.node_for_global = node
|
||||
else: #This is an extruder's quality changes profile.
|
||||
else: # This is an extruder's quality changes profile.
|
||||
self.nodes_for_extruders[extruder_position] = node
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
|
@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Optional, cast, Dict, List
|
|||
|
||||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||
from UM.Logger import Logger
|
||||
from UM.Util import parseBool
|
||||
|
@ -21,7 +20,6 @@ if TYPE_CHECKING:
|
|||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from .QualityChangesGroup import QualityChangesGroup
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
|
||||
#
|
||||
|
@ -38,11 +36,11 @@ class QualityManager(QObject):
|
|||
|
||||
qualitiesUpdated = pyqtSignal()
|
||||
|
||||
def __init__(self, container_registry: "ContainerRegistry", parent = None) -> None:
|
||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._application = Application.getInstance() # type: CuraApplication
|
||||
self._application = application
|
||||
self._material_manager = self._application.getMaterialManager()
|
||||
self._container_registry = container_registry
|
||||
self._container_registry = self._application.getContainerRegistry()
|
||||
|
||||
self._empty_quality_container = self._application.empty_quality_container
|
||||
self._empty_quality_changes_container = self._application.empty_quality_changes_container
|
||||
|
@ -458,7 +456,7 @@ class QualityManager(QObject):
|
|||
# stack and clear the user settings.
|
||||
@pyqtSlot(str)
|
||||
def createQualityChanges(self, base_name: str) -> None:
|
||||
machine_manager = Application.getInstance().getMachineManager()
|
||||
machine_manager = self._application.getMachineManager()
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
if not global_stack:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional, Dict, cast
|
||||
from typing import Optional, Dict, cast, Any
|
||||
|
||||
from .ContainerNode import ContainerNode
|
||||
from .QualityChangesGroup import QualityChangesGroup
|
||||
|
@ -12,21 +12,21 @@ from .QualityChangesGroup import QualityChangesGroup
|
|||
#
|
||||
class QualityNode(ContainerNode):
|
||||
|
||||
def __init__(self, metadata: Optional[dict] = None) -> None:
|
||||
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
|
||||
super().__init__(metadata = metadata)
|
||||
self.quality_type_map = {} # type: Dict[str, QualityNode] # quality_type -> QualityNode for InstanceContainer
|
||||
|
||||
def getChildNode(self, child_key: str) -> Optional["QualityNode"]:
|
||||
return self.children_map.get(child_key)
|
||||
|
||||
def addQualityMetadata(self, quality_type: str, metadata: dict):
|
||||
def addQualityMetadata(self, quality_type: str, metadata: Dict[str, Any]):
|
||||
if quality_type not in self.quality_type_map:
|
||||
self.quality_type_map[quality_type] = QualityNode(metadata)
|
||||
|
||||
def getQualityNode(self, quality_type: str) -> Optional["QualityNode"]:
|
||||
return self.quality_type_map.get(quality_type)
|
||||
|
||||
def addQualityChangesMetadata(self, quality_type: str, metadata: dict):
|
||||
def addQualityChangesMetadata(self, quality_type: str, metadata: Dict[str, Any]):
|
||||
if quality_type not in self.quality_type_map:
|
||||
self.quality_type_map[quality_type] = QualityNode()
|
||||
quality_type_node = self.quality_type_map[quality_type]
|
||||
|
|
|
@ -115,17 +115,24 @@ class VariantManager:
|
|||
|
||||
#
|
||||
# Gets the default variant for the given machine definition.
|
||||
# If the optional GlobalStack is given, the metadata information will be fetched from the GlobalStack instead of
|
||||
# the DefinitionContainer. Because for machines such as UM2, you can enable Olsson Block, which will set
|
||||
# "has_variants" to True in the GlobalStack. In those cases, we need to fetch metadata from the GlobalStack or
|
||||
# it may not be correct.
|
||||
#
|
||||
def getDefaultVariantNode(self, machine_definition: "DefinitionContainer",
|
||||
variant_type: VariantType) -> Optional["ContainerNode"]:
|
||||
variant_type: "VariantType",
|
||||
global_stack: Optional["GlobalStack"] = None) -> Optional["ContainerNode"]:
|
||||
machine_definition_id = machine_definition.getId()
|
||||
container_for_metadata_fetching = global_stack if global_stack is not None else machine_definition
|
||||
|
||||
preferred_variant_name = None
|
||||
if variant_type == VariantType.BUILD_PLATE:
|
||||
if parseBool(machine_definition.getMetaDataEntry("has_variant_buildplates", False)):
|
||||
preferred_variant_name = machine_definition.getMetaDataEntry("preferred_variant_buildplate_name")
|
||||
if parseBool(container_for_metadata_fetching.getMetaDataEntry("has_variant_buildplates", False)):
|
||||
preferred_variant_name = container_for_metadata_fetching.getMetaDataEntry("preferred_variant_buildplate_name")
|
||||
else:
|
||||
if parseBool(machine_definition.getMetaDataEntry("has_variants", False)):
|
||||
preferred_variant_name = machine_definition.getMetaDataEntry("preferred_variant_name")
|
||||
if parseBool(container_for_metadata_fetching.getMetaDataEntry("has_variants", False)):
|
||||
preferred_variant_name = container_for_metadata_fetching.getMetaDataEntry("preferred_variant_name")
|
||||
|
||||
node = None
|
||||
if preferred_variant_name:
|
||||
|
|
112
cura/OAuth2/AuthorizationHelpers.py
Normal file
112
cura/OAuth2/AuthorizationHelpers.py
Normal file
|
@ -0,0 +1,112 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import json
|
||||
import random
|
||||
from hashlib import sha512
|
||||
from base64 import b64encode
|
||||
from typing import Dict, Optional
|
||||
|
||||
import requests
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
|
||||
|
||||
|
||||
# Class containing several helpers to deal with the authorization flow.
|
||||
class AuthorizationHelpers:
|
||||
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":
|
||||
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":
|
||||
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 "",
|
||||
"grant_type": "authorization_code",
|
||||
"code": authorization_code,
|
||||
"code_verifier": verification_code,
|
||||
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||
}
|
||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
||||
|
||||
# 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":
|
||||
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 "",
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": refresh_token,
|
||||
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||
}
|
||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
||||
|
||||
@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":
|
||||
token_data = None
|
||||
|
||||
try:
|
||||
token_data = json.loads(token_response.text)
|
||||
except ValueError:
|
||||
Logger.log("w", "Could not parse token response data: %s", token_response.text)
|
||||
|
||||
if not token_data:
|
||||
return AuthenticationResponse(success=False, err_message="Could not read response.")
|
||||
|
||||
if token_response.status_code not in (200, 201):
|
||||
return AuthenticationResponse(success=False, err_message=token_data["error_description"])
|
||||
|
||||
return AuthenticationResponse(success=True,
|
||||
token_type=token_data["token_type"],
|
||||
access_token=token_data["access_token"],
|
||||
refresh_token=token_data["refresh_token"],
|
||||
expires_in=token_data["expires_in"],
|
||||
scope=token_data["scope"])
|
||||
|
||||
# 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"]:
|
||||
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
if token_request.status_code not in (200, 201):
|
||||
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
|
||||
return None
|
||||
user_data = token_request.json().get("data")
|
||||
if not user_data or not isinstance(user_data, dict):
|
||||
Logger.log("w", "Could not parse user data from token: %s", user_data)
|
||||
return None
|
||||
return UserProfile(
|
||||
user_id = user_data["user_id"],
|
||||
username = user_data["username"],
|
||||
profile_image_url = user_data.get("profile_image_url", "")
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
# Generate a 16-character verification code.
|
||||
# \param code_length: How long should the code be?
|
||||
def generateVerificationCode(code_length: int = 16) -> str:
|
||||
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:
|
||||
encoded = sha512(verification_code.encode()).digest()
|
||||
return b64encode(encoded, altchars = b"_-").decode()
|
101
cura/OAuth2/AuthorizationRequestHandler.py
Normal file
101
cura/OAuth2/AuthorizationRequestHandler.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING
|
||||
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from cura.OAuth2.Models import AuthenticationResponse, ResponseData, HTTP_STATUS
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import ResponseStatus
|
||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
||||
|
||||
|
||||
# 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):
|
||||
def __init__(self, request, client_address, server) -> None:
|
||||
super().__init__(request, client_address, server)
|
||||
|
||||
# These values will be injected by the HTTPServer that this handler belongs to.
|
||||
self.authorization_helpers = None # type: Optional["AuthorizationHelpers"]
|
||||
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
|
||||
self.verification_code = None # type: Optional[str]
|
||||
|
||||
def do_GET(self) -> None:
|
||||
# Extract values from the query string.
|
||||
parsed_url = urlparse(self.path)
|
||||
query = parse_qs(parsed_url.query)
|
||||
|
||||
# Handle the possible requests
|
||||
if parsed_url.path == "/callback":
|
||||
server_response, token_response = self._handleCallback(query)
|
||||
else:
|
||||
server_response = self._handleNotFound()
|
||||
token_response = None
|
||||
|
||||
# Send the data to the browser.
|
||||
self._sendHeaders(server_response.status, server_response.content_type, server_response.redirect_uri)
|
||||
|
||||
if server_response.data_stream:
|
||||
# If there is data in the response, we send it.
|
||||
self._sendData(server_response.data_stream)
|
||||
|
||||
if token_response and self.authorization_callback is not None:
|
||||
# Trigger the callback if we got a response.
|
||||
# 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]]:
|
||||
code = self._queryGet(query, "code")
|
||||
if code and self.authorization_helpers is not None and self.verification_code is not None:
|
||||
# If the code was returned we get the access token.
|
||||
token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode(
|
||||
code, self.verification_code)
|
||||
|
||||
elif self._queryGet(query, "error_code") == "user_denied":
|
||||
# Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog).
|
||||
token_response = AuthenticationResponse(
|
||||
success=False,
|
||||
err_message="Please give the required permissions when authorizing this application."
|
||||
)
|
||||
|
||||
else:
|
||||
# We don't know what went wrong here, so instruct the user to check the logs.
|
||||
token_response = AuthenticationResponse(
|
||||
success=False,
|
||||
error_message="Something unexpected happened when trying to log in, please try again."
|
||||
)
|
||||
if self.authorization_helpers is None:
|
||||
return ResponseData(), token_response
|
||||
|
||||
return ResponseData(
|
||||
status=HTTP_STATUS["REDIRECT"],
|
||||
data_stream=b"Redirecting...",
|
||||
redirect_uri=self.authorization_helpers.settings.AUTH_SUCCESS_REDIRECT if token_response.success else
|
||||
self.authorization_helpers.settings.AUTH_FAILED_REDIRECT
|
||||
), token_response
|
||||
|
||||
@staticmethod
|
||||
# Handle all other non-existing server calls.
|
||||
def _handleNotFound() -> ResponseData:
|
||||
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:
|
||||
self.send_response(status.code, status.message)
|
||||
self.send_header("Content-type", content_type)
|
||||
if redirect_uri:
|
||||
self.send_header("Location", redirect_uri)
|
||||
self.end_headers()
|
||||
|
||||
def _sendData(self, data: bytes) -> None:
|
||||
self.wfile.write(data)
|
||||
|
||||
@staticmethod
|
||||
# Convenience Helper for getting values from a pre-parsed query string
|
||||
def _queryGet(query_data: Dict[Any, List], key: str, default: Optional[str]=None) -> Optional[str]:
|
||||
return query_data.get(key, [default])[0]
|
26
cura/OAuth2/AuthorizationRequestServer.py
Normal file
26
cura/OAuth2/AuthorizationRequestServer.py
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from http.server import HTTPServer
|
||||
from typing import Callable, Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import AuthenticationResponse
|
||||
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.
|
||||
def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None:
|
||||
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:
|
||||
self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore
|
||||
|
||||
# Set the verification code on the request handler.
|
||||
def setVerificationCode(self, verification_code: str) -> None:
|
||||
self.RequestHandlerClass.verification_code = verification_code # type: ignore
|
168
cura/OAuth2/AuthorizationService.py
Normal file
168
cura/OAuth2/AuthorizationService.py
Normal file
|
@ -0,0 +1,168 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import json
|
||||
import webbrowser
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import Signal
|
||||
|
||||
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
|
||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
||||
from cura.OAuth2.Models import AuthenticationResponse
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import UserProfile, OAuth2Settings
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
|
||||
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()
|
||||
|
||||
# Emit signal when authentication failed.
|
||||
onAuthenticationError = Signal()
|
||||
|
||||
def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None:
|
||||
self._settings = settings
|
||||
self._auth_helpers = AuthorizationHelpers(settings)
|
||||
self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL)
|
||||
self._auth_data = None # type: Optional[AuthenticationResponse]
|
||||
self._user_profile = None # type: Optional["UserProfile"]
|
||||
self._preferences = preferences
|
||||
self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True)
|
||||
|
||||
def initialize(self, preferences: Optional["Preferences"] = None) -> None:
|
||||
if preferences is not None:
|
||||
self._preferences = preferences
|
||||
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"]:
|
||||
if not self._user_profile:
|
||||
# If no user profile was stored locally, we try to get it from JWT.
|
||||
self._user_profile = self._parseJWT()
|
||||
if not self._user_profile:
|
||||
# If there is still no user profile from the JWT, we have to log in again.
|
||||
return None
|
||||
|
||||
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"]:
|
||||
if not self._auth_data or self._auth_data.access_token is None:
|
||||
# If no auth data exists, we should always log in again.
|
||||
return None
|
||||
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||
if user_data:
|
||||
# If the profile was found, we return it immediately.
|
||||
return user_data
|
||||
# The JWT was expired or invalid and we should request a new one.
|
||||
if self._auth_data.refresh_token is None:
|
||||
return None
|
||||
self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
||||
if not self._auth_data or self._auth_data.access_token is None:
|
||||
# The token could not be refreshed using the refresh token. We should login again.
|
||||
return None
|
||||
|
||||
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]:
|
||||
if not self.getUserProfile():
|
||||
# We check if we can get the user profile.
|
||||
# If we can't get it, that means the access token (JWT) was invalid or expired.
|
||||
return None
|
||||
|
||||
if self._auth_data is None:
|
||||
return None
|
||||
|
||||
return self._auth_data.access_token
|
||||
|
||||
# Try to refresh the access token. This should be used when it has expired.
|
||||
def refreshAccessToken(self) -> None:
|
||||
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
|
||||
self._storeAuthData(self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token))
|
||||
self.onAuthStateChanged.emit(logged_in=True)
|
||||
|
||||
# Delete the authentication data that we have stored locally (eg; logout)
|
||||
def deleteAuthData(self) -> None:
|
||||
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) -> None:
|
||||
Logger.log("d", "Starting new OAuth2 flow...")
|
||||
|
||||
# Create the tokens needed for the code challenge (PKCE) extension for OAuth2.
|
||||
# This is needed because the CuraDrivePlugin is a untrusted (open source) client.
|
||||
# More details can be found at https://tools.ietf.org/html/rfc7636.
|
||||
verification_code = self._auth_helpers.generateVerificationCode()
|
||||
challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code)
|
||||
|
||||
# Create the query string needed for the OAuth2 flow.
|
||||
query_string = urlencode({
|
||||
"client_id": self._settings.CLIENT_ID,
|
||||
"redirect_uri": self._settings.CALLBACK_URL,
|
||||
"scope": self._settings.CLIENT_SCOPES,
|
||||
"response_type": "code",
|
||||
"state": "CuraDriveIsAwesome",
|
||||
"code_challenge": challenge_code,
|
||||
"code_challenge_method": "S512"
|
||||
})
|
||||
|
||||
# Open the authorization page in a new browser window.
|
||||
webbrowser.open_new("{}?{}".format(self._auth_url, query_string))
|
||||
|
||||
# Start a local web server to receive the callback URL on.
|
||||
self._server.start(verification_code)
|
||||
|
||||
# Callback method for the authentication flow.
|
||||
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
|
||||
if auth_response.success:
|
||||
self._storeAuthData(auth_response)
|
||||
self.onAuthStateChanged.emit(logged_in=True)
|
||||
else:
|
||||
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:
|
||||
if self._preferences is None:
|
||||
Logger.log("e", "Unable to load authentication data, since no preference has been set!")
|
||||
return
|
||||
try:
|
||||
preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY))
|
||||
if preferences_data:
|
||||
self._auth_data = AuthenticationResponse(**preferences_data)
|
||||
self.onAuthStateChanged.emit(logged_in=True)
|
||||
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:
|
||||
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()
|
||||
self._preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(vars(auth_data)))
|
||||
else:
|
||||
self._user_profile = None
|
||||
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
|
64
cura/OAuth2/LocalAuthorizationServer.py
Normal file
64
cura/OAuth2/LocalAuthorizationServer.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import threading
|
||||
from typing import Optional, Callable, Any, TYPE_CHECKING
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
from cura.OAuth2.AuthorizationRequestServer import AuthorizationRequestServer
|
||||
from cura.OAuth2.AuthorizationRequestHandler import AuthorizationRequestHandler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import AuthenticationResponse
|
||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
||||
|
||||
|
||||
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:
|
||||
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
|
||||
self._auth_helpers = auth_helpers
|
||||
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.
|
||||
def start(self, verification_code: str) -> None:
|
||||
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.
|
||||
self._web_server.setVerificationCode(verification_code)
|
||||
return
|
||||
|
||||
if self._web_server_port is None:
|
||||
raise Exception("Unable to start server without specifying the port.")
|
||||
|
||||
Logger.log("d", "Starting local web server to handle authorization callback on port %s", self._web_server_port)
|
||||
|
||||
# Create the server and inject the callback and code.
|
||||
self._web_server = AuthorizationRequestServer(("0.0.0.0", self._web_server_port), AuthorizationRequestHandler)
|
||||
self._web_server.setAuthorizationHelpers(self._auth_helpers)
|
||||
self._web_server.setAuthorizationCallback(self._auth_state_changed_callback)
|
||||
self._web_server.setVerificationCode(verification_code)
|
||||
|
||||
# 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.start()
|
||||
|
||||
# Stops the web server if it was running. It also does some cleanup.
|
||||
def stop(self) -> None:
|
||||
Logger.log("d", "Stopping local oauth2 web server...")
|
||||
|
||||
if self._web_server:
|
||||
self._web_server.server_close()
|
||||
self._web_server = None
|
||||
self._web_server_thread = None
|
60
cura/OAuth2/Models.py
Normal file
60
cura/OAuth2/Models.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class BaseModel:
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
|
||||
# OAuth OAuth2Settings data template.
|
||||
class OAuth2Settings(BaseModel):
|
||||
CALLBACK_PORT = None # type: Optional[int]
|
||||
OAUTH_SERVER_URL = None # type: Optional[str]
|
||||
CLIENT_ID = None # type: Optional[str]
|
||||
CLIENT_SCOPES = None # type: Optional[str]
|
||||
CALLBACK_URL = None # type: Optional[str]
|
||||
AUTH_DATA_PREFERENCE_KEY = "" # type: str
|
||||
AUTH_SUCCESS_REDIRECT = "https://ultimaker.com" # type: str
|
||||
AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str
|
||||
|
||||
|
||||
# User profile data template.
|
||||
class UserProfile(BaseModel):
|
||||
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."""
|
||||
success = True # type: bool
|
||||
token_type = None # type: Optional[str]
|
||||
access_token = None # type: Optional[str]
|
||||
refresh_token = None # type: Optional[str]
|
||||
expires_in = None # type: Optional[str]
|
||||
scope = None # type: Optional[str]
|
||||
err_message = None # type: Optional[str]
|
||||
|
||||
|
||||
# Response status template.
|
||||
class ResponseStatus(BaseModel):
|
||||
code = 200 # type: int
|
||||
message = "" # type str
|
||||
|
||||
|
||||
# Response data template.
|
||||
class ResponseData(BaseModel):
|
||||
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 = {
|
||||
"OK": ResponseStatus(code=200, message="OK"),
|
||||
"NOT_FOUND": ResponseStatus(code=404, message="NOT FOUND"),
|
||||
"REDIRECT": ResponseStatus(code=302, message="REDIRECT")
|
||||
}
|
2
cura/OAuth2/__init__.py
Normal file
2
cura/OAuth2/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
@ -9,6 +9,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.i18n import i18nCatalog
|
||||
from collections import defaultdict
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -40,6 +41,8 @@ class ObjectsModel(ListModel):
|
|||
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
|
||||
active_build_plate_number = self._build_plate_number
|
||||
group_nr = 1
|
||||
name_count_dict = defaultdict(int)
|
||||
|
||||
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
|
@ -55,6 +58,7 @@ class ObjectsModel(ListModel):
|
|||
|
||||
if not node.callDecoration("isGroup"):
|
||||
name = node.getName()
|
||||
|
||||
else:
|
||||
name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr))
|
||||
group_nr += 1
|
||||
|
@ -63,6 +67,14 @@ class ObjectsModel(ListModel):
|
|||
is_outside_build_area = node.isOutsideBuildArea()
|
||||
else:
|
||||
is_outside_build_area = False
|
||||
|
||||
#check if we already have an instance of the object based on name
|
||||
name_count_dict[name] += 1
|
||||
name_count = name_count_dict[name]
|
||||
|
||||
if name_count > 1:
|
||||
name = "{0}({1})".format(name, name_count-1)
|
||||
node.setName(name)
|
||||
|
||||
nodes.append({
|
||||
"name": name,
|
||||
|
@ -71,6 +83,7 @@ class ObjectsModel(ListModel):
|
|||
"buildPlateNumber": node_build_plate_number,
|
||||
"node": node
|
||||
})
|
||||
|
||||
nodes = sorted(nodes, key=lambda n: n["name"])
|
||||
self.setItems(nodes)
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ from typing import Dict
|
|||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.Duration import Duration
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
@ -52,6 +51,8 @@ class PrintInformation(QObject):
|
|||
super().__init__(parent)
|
||||
self._application = application
|
||||
|
||||
self.UNTITLED_JOB_NAME = "Untitled"
|
||||
|
||||
self.initializeCuraMessagePrintTimeProperties()
|
||||
|
||||
self._material_lengths = {} # indexed by build plate number
|
||||
|
@ -70,12 +71,13 @@ class PrintInformation(QObject):
|
|||
self._base_name = ""
|
||||
self._abbr_machine = ""
|
||||
self._job_name = ""
|
||||
self._project_name = ""
|
||||
self._active_build_plate = 0
|
||||
self._initVariablesWithBuildPlate(self._active_build_plate)
|
||||
|
||||
self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
|
||||
|
||||
ss = self._multi_build_plate_model.maxBuildPlate
|
||||
|
||||
self._application.globalContainerStackChanged.connect(self._updateJobName)
|
||||
self._application.globalContainerStackChanged.connect(self.setToZeroPrintInformation)
|
||||
self._application.fileLoaded.connect(self.setBaseName)
|
||||
|
@ -300,13 +302,13 @@ class PrintInformation(QObject):
|
|||
|
||||
def _updateJobName(self):
|
||||
if self._base_name == "":
|
||||
self._job_name = "Untitled"
|
||||
self._job_name = self.UNTITLED_JOB_NAME
|
||||
self._is_user_specified_job_name = False
|
||||
self.jobNameChanged.emit()
|
||||
return
|
||||
|
||||
base_name = self._stripAccents(self._base_name)
|
||||
self._setAbbreviatedMachineName()
|
||||
self._defineAbbreviatedMachineName()
|
||||
|
||||
# Only update the job name when it's not user-specified.
|
||||
if not self._is_user_specified_job_name:
|
||||
|
@ -382,7 +384,7 @@ class PrintInformation(QObject):
|
|||
## Created an acronym-like abbreviated machine name from the currently
|
||||
# active machine name.
|
||||
# Called each time the global stack is switched.
|
||||
def _setAbbreviatedMachineName(self):
|
||||
def _defineAbbreviatedMachineName(self):
|
||||
global_container_stack = self._application.getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
self._abbr_machine = ""
|
||||
|
|
|
@ -13,20 +13,20 @@ class ConfigurationModel(QObject):
|
|||
|
||||
configurationChanged = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._printer_type = None
|
||||
self._printer_type = ""
|
||||
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
|
||||
self._buildplate_configuration = None
|
||||
self._buildplate_configuration = ""
|
||||
|
||||
def setPrinterType(self, printer_type):
|
||||
self._printer_type = printer_type
|
||||
|
||||
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
|
||||
def printerType(self):
|
||||
def printerType(self) -> str:
|
||||
return self._printer_type
|
||||
|
||||
def setExtruderConfigurations(self, extruder_configurations):
|
||||
def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]):
|
||||
if self._extruder_configurations != extruder_configurations:
|
||||
self._extruder_configurations = extruder_configurations
|
||||
|
||||
|
@ -39,16 +39,16 @@ class ConfigurationModel(QObject):
|
|||
def extruderConfigurations(self):
|
||||
return self._extruder_configurations
|
||||
|
||||
def setBuildplateConfiguration(self, buildplate_configuration):
|
||||
def setBuildplateConfiguration(self, buildplate_configuration: str) -> None:
|
||||
self._buildplate_configuration = buildplate_configuration
|
||||
|
||||
@pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged)
|
||||
def buildplateConfiguration(self):
|
||||
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):
|
||||
def isValid(self) -> bool:
|
||||
if not self._extruder_configurations:
|
||||
return False
|
||||
for configuration in self._extruder_configurations:
|
||||
|
|
|
@ -66,7 +66,7 @@ class GenericOutputController(PrinterOutputController):
|
|||
self._output_device.sendCommand("G28 Z")
|
||||
|
||||
def sendRawCommand(self, printer: "PrinterOutputModel", command: str):
|
||||
self._output_device.sendCommand(command)
|
||||
self._output_device.sendCommand(command.upper()) #Most printers only understand uppercase g-code commands.
|
||||
|
||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
||||
if state == "pause":
|
||||
|
|
|
@ -53,21 +53,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
self._sending_gcode = False
|
||||
self._compressing_gcode = False
|
||||
self._gcode = [] # type: List[str]
|
||||
|
||||
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
|
||||
|
||||
printer_type = self._properties.get(b"machine", b"").decode("utf-8")
|
||||
printer_type_identifiers = {
|
||||
"9066": "ultimaker3",
|
||||
"9511": "ultimaker3_extended",
|
||||
"9051": "ultimaker_s5"
|
||||
}
|
||||
self._printer_type = "Unknown"
|
||||
for key, value in printer_type_identifiers.items():
|
||||
if printer_type.startswith(key):
|
||||
self._printer_type = value
|
||||
break
|
||||
|
||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
||||
raise NotImplementedError("requestWrite needs to be implemented")
|
||||
|
||||
|
@ -341,7 +328,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def printerType(self) -> str:
|
||||
return self._printer_type
|
||||
return self._properties.get(b"printer_type", b"Unknown").decode("utf-8")
|
||||
|
||||
## IP adress of this printer
|
||||
@pyqtProperty(str, constant = True)
|
||||
|
|
|
@ -5,6 +5,7 @@ from PyQt5.QtCore import QTimer
|
|||
|
||||
from UM.Application import Application
|
||||
from UM.Math.Polygon import Polygon
|
||||
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
|
@ -13,39 +14,49 @@ from cura.Scene import ConvexHullNode
|
|||
|
||||
import numpy
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from UM.Mesh.MeshData import MeshData
|
||||
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):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._convex_hull_node = None
|
||||
self._convex_hull_node = None # type: Optional["SceneNode"]
|
||||
self._init2DConvexHullCache()
|
||||
|
||||
self._global_stack = None
|
||||
self._global_stack = None # type: Optional[GlobalStack]
|
||||
|
||||
# Make sure the timer is created on the main thread
|
||||
self._recompute_convex_hull_timer = None
|
||||
Application.getInstance().callLater(self.createRecomputeConvexHullTimer)
|
||||
self._recompute_convex_hull_timer = None # type: Optional[QTimer]
|
||||
from cura.CuraApplication import CuraApplication
|
||||
if CuraApplication.getInstance() is not None:
|
||||
CuraApplication.getInstance().callLater(self.createRecomputeConvexHullTimer)
|
||||
|
||||
self._raft_thickness = 0.0
|
||||
# For raft thickness, DRY
|
||||
self._build_volume = Application.getInstance().getBuildVolume()
|
||||
self._build_volume = CuraApplication.getInstance().getBuildVolume()
|
||||
self._build_volume.raftThicknessChanged.connect(self._onChanged)
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
Application.getInstance().getController().toolOperationStarted.connect(self._onChanged)
|
||||
Application.getInstance().getController().toolOperationStopped.connect(self._onChanged)
|
||||
CuraApplication.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
CuraApplication.getInstance().getController().toolOperationStarted.connect(self._onChanged)
|
||||
CuraApplication.getInstance().getController().toolOperationStopped.connect(self._onChanged)
|
||||
|
||||
self._onGlobalStackChanged()
|
||||
|
||||
def createRecomputeConvexHullTimer(self):
|
||||
def createRecomputeConvexHullTimer(self) -> None:
|
||||
self._recompute_convex_hull_timer = QTimer()
|
||||
self._recompute_convex_hull_timer.setInterval(200)
|
||||
self._recompute_convex_hull_timer.setSingleShot(True)
|
||||
self._recompute_convex_hull_timer.timeout.connect(self.recomputeConvexHull)
|
||||
|
||||
def setNode(self, node):
|
||||
def setNode(self, node: "SceneNode") -> None:
|
||||
previous_node = self._node
|
||||
# Disconnect from previous node signals
|
||||
if previous_node is not None and node is not previous_node:
|
||||
|
@ -53,9 +64,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
previous_node.parentChanged.disconnect(self._onChanged)
|
||||
|
||||
super().setNode(node)
|
||||
|
||||
self._node.transformationChanged.connect(self._onChanged)
|
||||
self._node.parentChanged.connect(self._onChanged)
|
||||
# Mypy doesn't understand that self._node is no longer optional, so just use the node.
|
||||
node.transformationChanged.connect(self._onChanged)
|
||||
node.parentChanged.connect(self._onChanged)
|
||||
|
||||
self._onChanged()
|
||||
|
||||
|
@ -63,37 +74,46 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
def __deepcopy__(self, memo):
|
||||
return ConvexHullDecorator()
|
||||
|
||||
## Get the unmodified 2D projected convex hull of the node
|
||||
def getConvexHull(self):
|
||||
## Get the unmodified 2D projected convex hull of the node (if any)
|
||||
def getConvexHull(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
hull = self._compute2DConvexHull()
|
||||
|
||||
if self._global_stack and self._node:
|
||||
if self._global_stack and self._node is not None and hull is not None:
|
||||
# Parent can be None if node is just loaded.
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")):
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32)))
|
||||
hull = self._add2DAdhesionMargin(hull)
|
||||
return hull
|
||||
|
||||
## Get the convex hull of the node with the full head size
|
||||
def getConvexHullHeadFull(self):
|
||||
def getConvexHullHeadFull(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
return self._compute2DConvexHeadFull()
|
||||
|
||||
@staticmethod
|
||||
def hasGroupAsParent(node: "SceneNode") -> bool:
|
||||
parent = node.getParent()
|
||||
if parent is None:
|
||||
return False
|
||||
return bool(parent.callDecoration("isGroup"))
|
||||
|
||||
## Get convex hull of the object + head size
|
||||
# In case of printing all at once this is the same as the convex hull.
|
||||
# For one at the time this is area with intersection of mirrored head
|
||||
def getConvexHullHead(self):
|
||||
def getConvexHullHead(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
if self._global_stack:
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")):
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
head_with_fans = self._compute2DConvexHeadMin()
|
||||
if head_with_fans is None:
|
||||
return None
|
||||
head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans)
|
||||
return head_with_fans_with_adhesion_margin
|
||||
return None
|
||||
|
@ -101,23 +121,24 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
## Get convex hull of the node
|
||||
# In case of printing all at once this is the same as the convex hull.
|
||||
# For one at the time this is the area without the head.
|
||||
def getConvexHullBoundary(self):
|
||||
def getConvexHullBoundary(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
if self._global_stack:
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")):
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
# Printing one at a time and it's not an object in a group
|
||||
return self._compute2DConvexHull()
|
||||
return None
|
||||
|
||||
def recomputeConvexHullDelayed(self):
|
||||
## The same as recomputeConvexHull, but using a timer if it was set.
|
||||
def recomputeConvexHullDelayed(self) -> None:
|
||||
if self._recompute_convex_hull_timer is not None:
|
||||
self._recompute_convex_hull_timer.start()
|
||||
else:
|
||||
self.recomputeConvexHull()
|
||||
|
||||
def recomputeConvexHull(self):
|
||||
def recomputeConvexHull(self) -> None:
|
||||
controller = Application.getInstance().getController()
|
||||
root = controller.getScene().getRoot()
|
||||
if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node):
|
||||
|
@ -132,27 +153,29 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
hull_node = ConvexHullNode.ConvexHullNode(self._node, convex_hull, self._raft_thickness, root)
|
||||
self._convex_hull_node = hull_node
|
||||
|
||||
def _onSettingValueChanged(self, key, property_name):
|
||||
if property_name != "value": #Not the value that was changed.
|
||||
def _onSettingValueChanged(self, key: str, property_name: str) -> None:
|
||||
if property_name != "value": # Not the value that was changed.
|
||||
return
|
||||
|
||||
if key in self._affected_settings:
|
||||
self._onChanged()
|
||||
if key in self._influencing_settings:
|
||||
self._init2DConvexHullCache() #Invalidate the cache.
|
||||
self._init2DConvexHullCache() # Invalidate the cache.
|
||||
self._onChanged()
|
||||
|
||||
def _init2DConvexHullCache(self):
|
||||
def _init2DConvexHullCache(self) -> None:
|
||||
# Cache for the group code path in _compute2DConvexHull()
|
||||
self._2d_convex_hull_group_child_polygon = None
|
||||
self._2d_convex_hull_group_result = None
|
||||
self._2d_convex_hull_group_child_polygon = None # type: Optional[Polygon]
|
||||
self._2d_convex_hull_group_result = None # type: Optional[Polygon]
|
||||
|
||||
# Cache for the mesh code path in _compute2DConvexHull()
|
||||
self._2d_convex_hull_mesh = None
|
||||
self._2d_convex_hull_mesh_world_transform = None
|
||||
self._2d_convex_hull_mesh_result = None
|
||||
self._2d_convex_hull_mesh = None # type: Optional[MeshData]
|
||||
self._2d_convex_hull_mesh_world_transform = None # type: Optional[Matrix]
|
||||
self._2d_convex_hull_mesh_result = None # type: Optional[Polygon]
|
||||
|
||||
def _compute2DConvexHull(self):
|
||||
def _compute2DConvexHull(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
if self._node.callDecoration("isGroup"):
|
||||
points = numpy.zeros((0, 2), dtype=numpy.int32)
|
||||
for child in self._node.getChildren():
|
||||
|
@ -178,49 +201,47 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return offset_hull
|
||||
|
||||
else:
|
||||
offset_hull = None
|
||||
mesh = None
|
||||
world_transform = None
|
||||
if self._node.getMeshData():
|
||||
mesh = self._node.getMeshData()
|
||||
world_transform = self._node.getWorldTransformation()
|
||||
|
||||
# Check the cache
|
||||
if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:
|
||||
return self._2d_convex_hull_mesh_result
|
||||
|
||||
vertex_data = mesh.getConvexHullTransformedVertices(world_transform)
|
||||
# Don't use data below 0.
|
||||
# TODO; We need a better check for this as this gives poor results for meshes with long edges.
|
||||
# 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:
|
||||
# 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.
|
||||
vertex_data = numpy.round(vertex_data, 1)
|
||||
|
||||
vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D.
|
||||
|
||||
# Grab the set of unique points.
|
||||
#
|
||||
# This basically finds the unique rows in the array by treating them as opaque groups of bytes
|
||||
# which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch.
|
||||
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
||||
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
||||
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
||||
_, idx = numpy.unique(vertex_byte_view, return_index=True)
|
||||
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
||||
|
||||
hull = Polygon(vertex_data)
|
||||
|
||||
if len(vertex_data) >= 3:
|
||||
convex_hull = hull.getConvexHull()
|
||||
offset_hull = self._offsetHull(convex_hull)
|
||||
else:
|
||||
offset_hull = Polygon([])
|
||||
mesh = self._node.getMeshData()
|
||||
if mesh is None:
|
||||
return Polygon([]) # Node has no mesh data, so just return an empty Polygon.
|
||||
|
||||
world_transform = self._node.getWorldTransformation()
|
||||
|
||||
# Check the cache
|
||||
if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:
|
||||
return self._2d_convex_hull_mesh_result
|
||||
|
||||
vertex_data = mesh.getConvexHullTransformedVertices(world_transform)
|
||||
# Don't use data below 0.
|
||||
# TODO; We need a better check for this as this gives poor results for meshes with long edges.
|
||||
# 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.
|
||||
# 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.
|
||||
vertex_data = numpy.round(vertex_data, 1)
|
||||
|
||||
vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D.
|
||||
|
||||
# Grab the set of unique points.
|
||||
#
|
||||
# This basically finds the unique rows in the array by treating them as opaque groups of bytes
|
||||
# which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch.
|
||||
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
||||
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
||||
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
||||
_, idx = numpy.unique(vertex_byte_view, return_index=True)
|
||||
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
||||
|
||||
hull = Polygon(vertex_data)
|
||||
|
||||
if len(vertex_data) >= 3:
|
||||
convex_hull = hull.getConvexHull()
|
||||
offset_hull = self._offsetHull(convex_hull)
|
||||
|
||||
# Store the result in the cache
|
||||
self._2d_convex_hull_mesh = mesh
|
||||
self._2d_convex_hull_mesh_world_transform = world_transform
|
||||
|
@ -228,24 +249,33 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
|
||||
return offset_hull
|
||||
|
||||
def _getHeadAndFans(self):
|
||||
return Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32))
|
||||
def _getHeadAndFans(self) -> Polygon:
|
||||
if self._global_stack:
|
||||
return Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32))
|
||||
return Polygon()
|
||||
|
||||
def _compute2DConvexHeadFull(self):
|
||||
return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans())
|
||||
def _compute2DConvexHeadFull(self) -> Optional[Polygon]:
|
||||
convex_hull = self._compute2DConvexHull()
|
||||
if convex_hull:
|
||||
return convex_hull.getMinkowskiHull(self._getHeadAndFans())
|
||||
return None
|
||||
|
||||
def _compute2DConvexHeadMin(self):
|
||||
headAndFans = self._getHeadAndFans()
|
||||
mirrored = headAndFans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically.
|
||||
def _compute2DConvexHeadMin(self) -> Optional[Polygon]:
|
||||
head_and_fans = self._getHeadAndFans()
|
||||
mirrored = head_and_fans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically.
|
||||
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
|
||||
|
||||
# Min head hull is used for the push free
|
||||
min_head_hull = self._compute2DConvexHull().getMinkowskiHull(head_and_fans)
|
||||
return min_head_hull
|
||||
convex_hull = self._compute2DConvexHeadFull()
|
||||
if convex_hull:
|
||||
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):
|
||||
def _add2DAdhesionMargin(self, poly: Polygon) -> Polygon:
|
||||
if not self._global_stack:
|
||||
return Polygon()
|
||||
# Compensate for raft/skirt/brim
|
||||
# Add extra margin depending on adhesion type
|
||||
adhesion_type = self._global_stack.getProperty("adhesion_type", "value")
|
||||
|
@ -263,7 +293,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
else:
|
||||
raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?")
|
||||
|
||||
# adjust head_and_fans with extra margin
|
||||
# Adjust head_and_fans with extra margin
|
||||
if extra_margin > 0:
|
||||
extra_margin_polygon = Polygon.approximatedCircle(extra_margin)
|
||||
poly = poly.getMinkowskiHull(extra_margin_polygon)
|
||||
|
@ -274,7 +304,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
# \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):
|
||||
def _offsetHull(self, convex_hull: Polygon) -> Polygon:
|
||||
horizontal_expansion = max(
|
||||
self._getSettingProperty("xy_offset", "value"),
|
||||
self._getSettingProperty("xy_offset_layer_0", "value")
|
||||
|
@ -295,12 +325,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
else:
|
||||
return convex_hull
|
||||
|
||||
def _onChanged(self, *args):
|
||||
def _onChanged(self, *args) -> None:
|
||||
self._raft_thickness = self._build_volume.getRaftThickness()
|
||||
if not args or args[0] == self._node:
|
||||
self.recomputeConvexHullDelayed()
|
||||
|
||||
def _onGlobalStackChanged(self):
|
||||
def _onGlobalStackChanged(self) -> None:
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged)
|
||||
self._global_stack.containersChanged.disconnect(self._onChanged)
|
||||
|
@ -321,7 +351,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, prop = "value"):
|
||||
def _getSettingProperty(self, setting_key: str, prop: str = "value") -> Any:
|
||||
if self._global_stack is None or self._node is None:
|
||||
return None
|
||||
per_mesh_stack = self._node.callDecoration("getStack")
|
||||
if per_mesh_stack:
|
||||
return per_mesh_stack.getProperty(setting_key, prop)
|
||||
|
@ -339,8 +371,8 @@ 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, node):
|
||||
## Returns True if node is a descendant or the same as the root node.
|
||||
def __isDescendant(self, root: "SceneNode", node: Optional["SceneNode"]) -> bool:
|
||||
if node is None:
|
||||
return False
|
||||
if root is node:
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from typing import List
|
||||
|
||||
|
||||
class GCodeListDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._gcode_list = []
|
||||
self._gcode_list = [] # type: List[str]
|
||||
|
||||
def getGCodeList(self):
|
||||
def getGCodeList(self) -> List[str]:
|
||||
return self._gcode_list
|
||||
|
||||
def setGCodeList(self, list):
|
||||
def setGCodeList(self, list: List[str]):
|
||||
self._gcode_list = list
|
||||
|
||||
def __deepcopy__(self, memo) -> "GCodeListDecorator":
|
||||
copied_decorator = GCodeListDecorator()
|
||||
copied_decorator.setGCodeList(self.getGCodeList())
|
||||
return copied_decorator
|
|
@ -2,11 +2,11 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
|||
|
||||
|
||||
class SliceableObjectDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
def isSliceable(self):
|
||||
def isSliceable(self) -> bool:
|
||||
return True
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
def __deepcopy__(self, memo) -> "SliceableObjectDecorator":
|
||||
return type(self)()
|
||||
|
|
|
@ -1,18 +1,19 @@
|
|||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
|
||||
|
||||
## A decorator that stores the amount an object has been moved below the platform.
|
||||
class ZOffsetDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._z_offset = 0
|
||||
self._z_offset = 0.
|
||||
|
||||
def setZOffset(self, offset):
|
||||
def setZOffset(self, offset: float) -> None:
|
||||
self._z_offset = offset
|
||||
|
||||
def getZOffset(self):
|
||||
def getZOffset(self) -> float:
|
||||
return self._z_offset
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
def __deepcopy__(self, memo) -> "ZOffsetDecorator":
|
||||
copied_decorator = ZOffsetDecorator()
|
||||
copied_decorator.setZOffset(self.getZOffset())
|
||||
return copied_decorator
|
||||
|
|
|
@ -28,10 +28,10 @@ if TYPE_CHECKING:
|
|||
from cura.Machines.MaterialNode import MaterialNode
|
||||
from cura.Machines.QualityChangesGroup import QualityChangesGroup
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from cura.Settings.MachineManager import MachineManager
|
||||
from cura.Machines.MaterialManager import MaterialManager
|
||||
from cura.Machines.QualityManager import QualityManager
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -52,7 +52,7 @@ class ContainerManager(QObject):
|
|||
|
||||
self._application = application # type: CuraApplication
|
||||
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
|
||||
self._container_registry = self._application.getContainerRegistry() # type: ContainerRegistry
|
||||
self._container_registry = self._application.getContainerRegistry() # type: CuraContainerRegistry
|
||||
self._machine_manager = self._application.getMachineManager() # type: MachineManager
|
||||
self._material_manager = self._application.getMaterialManager() # type: MaterialManager
|
||||
self._quality_manager = self._application.getQualityManager() # type: QualityManager
|
||||
|
@ -391,7 +391,8 @@ class ContainerManager(QObject):
|
|||
continue
|
||||
|
||||
mime_type = self._container_registry.getMimeTypeForContainer(container_type)
|
||||
|
||||
if mime_type is None:
|
||||
continue
|
||||
entry = {
|
||||
"type": serialize_type,
|
||||
"mime": mime_type,
|
||||
|
|
|
@ -291,7 +291,7 @@ class CuraContainerStack(ContainerStack):
|
|||
|
||||
# Helper to make sure we emit a PyQt signal on container changes.
|
||||
def _onContainersChanged(self, container: Any) -> None:
|
||||
self.pyqtContainersChanged.emit()
|
||||
Application.getInstance().callLater(self.pyqtContainersChanged.emit)
|
||||
|
||||
# Helper that can be overridden to get the "machine" definition, that is, the definition that defines the machine
|
||||
# and its properties rather than, for example, the extruder. Defaults to simply returning the definition property.
|
||||
|
|
|
@ -114,7 +114,8 @@ class CuraStackBuilder:
|
|||
|
||||
# get variant container for extruders
|
||||
extruder_variant_container = application.empty_variant_container
|
||||
extruder_variant_node = variant_manager.getDefaultVariantNode(global_stack.definition, VariantType.NOZZLE)
|
||||
extruder_variant_node = variant_manager.getDefaultVariantNode(global_stack.definition, VariantType.NOZZLE,
|
||||
global_stack = global_stack)
|
||||
extruder_variant_name = None
|
||||
if extruder_variant_node:
|
||||
extruder_variant_container = extruder_variant_node.getContainer()
|
||||
|
|
|
@ -348,8 +348,19 @@ class ExtruderManager(QObject):
|
|||
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
|
||||
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
|
||||
def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
|
||||
extruder_stack_0 = global_stack.extruders.get("0")
|
||||
# At this point, extruder stacks for this machine may not have been loaded yet. In this case, need to look in
|
||||
# the container registry as well.
|
||||
if not global_stack.extruders:
|
||||
extruder_trains = container_registry.findContainerStacks(type = "extruder_train",
|
||||
machine = global_stack.getId())
|
||||
if extruder_trains:
|
||||
for extruder in extruder_trains:
|
||||
if extruder.getMetaDataEntry("position") == "0":
|
||||
extruder_stack_0 = extruder
|
||||
break
|
||||
|
||||
if extruder_stack_0 is None:
|
||||
Logger.log("i", "No extruder stack for global stack [%s], create one", global_stack.getId())
|
||||
|
@ -360,7 +371,6 @@ class ExtruderManager(QObject):
|
|||
elif extruder_stack_0.definition.getId() != expected_extruder_definition_0_id:
|
||||
Logger.log("e", "Single extruder printer [{printer}] expected extruder [{expected}], but got [{got}]. I'm making it [{expected}].".format(
|
||||
printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId()))
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
|
||||
extruder_stack_0.definition = extruder_definition
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ from .CuraContainerStack import CuraContainerStack
|
|||
if TYPE_CHECKING:
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
|
||||
|
||||
## Represents the Global or Machine stack and its related containers.
|
||||
#
|
||||
class GlobalStack(CuraContainerStack):
|
||||
|
|
|
@ -1148,7 +1148,7 @@ class MachineManager(QObject):
|
|||
self._fixQualityChangesGroupToNotSupported(quality_changes_group)
|
||||
|
||||
quality_changes_container = self._empty_quality_changes_container
|
||||
quality_container = self._empty_quality_container
|
||||
quality_container = self._empty_quality_container # type: Optional[InstanceContainer]
|
||||
if quality_changes_group.node_for_global and quality_changes_group.node_for_global.getContainer():
|
||||
quality_changes_container = cast(InstanceContainer, quality_changes_group.node_for_global.getContainer())
|
||||
if quality_group is not None and quality_group.node_for_global and quality_group.node_for_global.getContainer():
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue