Merge branch '3.6' into bb-3.6

# Conflicts:
#	plugins/ChangeLogPlugin/ChangeLog.txt
#	plugins/USBPrinting/USBPrinterOutputDeviceManager.py
#	resources/qml/Menus/SettingVisibilityPresetsMenu.qml
This commit is contained in:
fieldOfView 2018-11-13 15:25:24 +01:00
commit ede065a3e1
333 changed files with 34412 additions and 28264 deletions

104
Jenkinsfile vendored
View file

@ -1,8 +1,11 @@
parallel_nodes(['linux && cura', 'windows && cura']) {
timeout(time: 2, unit: "HOURS") {
parallel_nodes(['linux && cura', 'windows && cura'])
{
timeout(time: 2, unit: "HOURS")
{
// Prepare building
stage('Prepare') {
stage('Prepare')
{
// Ensure we start with a clean build directory.
step([$class: 'WsCleanup'])
@ -11,26 +14,17 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
}
// If any error occurs during building, we want to catch it and continue with the "finale" stage.
catchError {
stage('Pre Checks') {
if (isUnix()) {
try {
sh """
echo 'Check for duplicate shortcut keys in all translation files.'
${env.CURA_ENVIRONMENT_PATH}/master/bin/python3 scripts/check_shortcut_keys.py
"""
} catch(e) {
currentBuild.result = "UNSTABLE"
}
}
}
catchError
{
// Building and testing should happen in a subdirectory.
dir('build') {
dir('build')
{
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.
stage('Build') {
stage('Build')
{
def branch = env.BRANCH_NAME
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
{
branch = "master"
}
@ -40,18 +34,76 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
}
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
stage('Unit Test') {
try {
make('test')
} catch(e) {
currentBuild.result = "UNSTABLE"
stage('Unit Test')
{
if (isUnix())
{
// For Linux to show everything
def branch = env.BRANCH_NAME
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
{
branch = "master"
}
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
try {
sh """
cd ..
export PYTHONPATH=.:"${uranium_dir}"
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests
"""
} catch(e)
{
currentBuild.result = "UNSTABLE"
}
}
else
{
// For Windows
try
{
// This also does code style checks.
bat 'ctest -V'
} catch(e)
{
currentBuild.result = "UNSTABLE"
}
}
}
stage('Code Style')
{
if (isUnix())
{
// For Linux to show everything.
// CMake also runs this test, but if it fails then the test just shows "failed" without details of what exactly failed.
def branch = env.BRANCH_NAME
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
{
branch = "master"
}
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
try
{
sh """
cd ..
export PYTHONPATH=.:"${uranium_dir}"
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/python3 run_mypy.py
"""
}
catch(e)
{
currentBuild.result = "UNSTABLE"
}
}
}
}
}
// Perform any post-build actions like notification and publishing of unit tests.
stage('Finalize') {
stage('Finalize')
{
// Publish the test results to Jenkins.
junit allowEmptyResults: true, testResults: 'build/junit*.xml'

View file

@ -34,7 +34,7 @@ function(cura_add_test)
if (NOT ${test_exists})
add_test(
NAME ${_NAME}
COMMAND ${PYTHON_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
COMMAND ${PYTHON_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
)
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
@ -57,5 +57,13 @@ endforeach()
#Add code style test.
add_test(
NAME "code-style"
COMMAND ${PYTHON_EXECUTABLE} run_mypy.py WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMAND ${PYTHON_EXECUTABLE} run_mypy.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)
#Add test for whether the shortcut alt-keys are unique in every translation.
add_test(
NAME "shortcut-keys"
COMMAND ${PYTHON_EXECUTABLE} scripts/check_shortcut_keys.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
)

View file

@ -13,6 +13,6 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
Icon=cura-icon
Terminal=false
Type=Application
MimeType=application/sla;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;
Categories=Graphics;
Keywords=3D;Printing;Slicer;

View file

@ -6,7 +6,7 @@
<glob-deleteall/>
<glob pattern="*.3mf"/>
</mime-type>
<mime-type type="application/sla">
<mime-type type="model/stl">
<comment>Computer-aided design and manufacturing format</comment>
<icon name="unknown"/>
<glob-deleteall/>

117
cura/API/Account.py Normal file
View 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="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.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()

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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)

View 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.

View file

@ -28,7 +28,7 @@ import copy
from typing import List, Optional
# Setting for clearance around the prime
# Radius of disallowed area in mm around prime. I.e. how much distance to keep from prime position.
PRIME_CLEARANCE = 6.5
@ -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:
@ -720,21 +718,23 @@ class BuildVolume(SceneNode):
# Add prime tower location as disallowed area.
if len(used_extruders) > 1: #No prime tower in single-extrusion.
prime_tower_collision = False
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
for extruder_id in prime_tower_areas:
for prime_tower_area in prime_tower_areas[extruder_id]:
for area in result_areas[extruder_id]:
if prime_tower_area.intersectsPolygon(area) is not None:
prime_tower_collision = True
if len([x for x in used_extruders if x.isEnabled == True]) > 1: #No prime tower if only one extruder is enabled
prime_tower_collision = False
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
for extruder_id in prime_tower_areas:
for prime_tower_area in prime_tower_areas[extruder_id]:
for area in result_areas[extruder_id]:
if prime_tower_area.intersectsPolygon(area) is not None:
prime_tower_collision = True
break
if prime_tower_collision: #Already found a collision.
break
if prime_tower_collision: #Already found a collision.
break
if not prime_tower_collision:
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
else:
self._error_areas.extend(prime_tower_areas[extruder_id])
if not prime_tower_collision:
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
else:
self._error_areas.extend(prime_tower_areas[extruder_id])
self._has_errors = len(self._error_areas) > 0

View file

@ -1,29 +0,0 @@
from PyQt5.QtGui import QImage
from PyQt5.QtQuick import QQuickImageProvider
from PyQt5.QtCore import QSize
from UM.Application import Application
class CameraImageProvider(QQuickImageProvider):
def __init__(self):
super().__init__(QQuickImageProvider.Image)
## Request a new image.
def requestImage(self, id, size):
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
try:
image = output_device.activePrinter.camera.getImage()
if image.isNull():
image = QImage()
return image, QSize(15, 15)
except AttributeError:
try:
image = output_device.activeCamera.getImage()
return image, QSize(15, 15)
except AttributeError:
pass
return QImage(), QSize(15, 15)

View file

@ -4,7 +4,7 @@
import os
import sys
import time
from typing import cast, TYPE_CHECKING
from typing import cast, TYPE_CHECKING, Optional, Callable
import numpy
@ -13,6 +13,7 @@ from PyQt5.QtGui import QColor, QIcon
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
from UM.Application import Application
from UM.PluginError import PluginNotFoundError
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Camera import Camera
@ -44,6 +45,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 +63,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
@ -93,7 +96,6 @@ from . import PrintInformation
from . import CuraActions
from cura.Scene import ZOffsetDecorator
from . import CuraSplashScreen
from . import CameraImageProvider
from . import PrintJobPreviewImageProvider
from . import MachineActionManager
@ -107,16 +109,26 @@ from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisi
from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
import cura.Settings.cura_empty_instance_containers
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
from cura.ObjectsModel import ObjectsModel
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
from UM.FlameProfiler import pyqtSlot
from UM.Decorators import override
if TYPE_CHECKING:
from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
from cura.Settings.GlobalStack import GlobalStack
numpy.seterr(all = "ignore")
try:
from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion
from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion # type: ignore
except ImportError:
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
CuraBuildType = ""
@ -157,6 +169,8 @@ class CuraApplication(QtApplication):
self.default_theme = "cura-light"
self.change_log_url = "https://ultimaker.com/ultimaker-cura-latest-features"
self._boot_loading_time = time.time()
self._on_exit_callback_manager = OnExitCallbackManager(self)
@ -168,16 +182,18 @@ class CuraApplication(QtApplication):
self._single_instance = None
self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions]
self._cura_package_manager = None
self._machine_action_manager = None
self.empty_container = None
self.empty_definition_changes_container = None
self.empty_variant_container = None
self.empty_material_container = None
self.empty_quality_container = None
self.empty_quality_changes_container = None
self.empty_container = None # type: EmptyInstanceContainer
self.empty_definition_changes_container = None # type: EmptyInstanceContainer
self.empty_variant_container = None # type: EmptyInstanceContainer
self.empty_material_container = None # type: EmptyInstanceContainer
self.empty_quality_container = None # type: EmptyInstanceContainer
self.empty_quality_changes_container = None # type: EmptyInstanceContainer
self._variant_manager = None
self._material_manager = None
@ -197,6 +213,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
@ -235,6 +252,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
@ -259,6 +278,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()
@ -285,8 +307,6 @@ class CuraApplication(QtApplication):
self._machine_action_manager = MachineActionManager.MachineActionManager(self)
self._machine_action_manager.initialize()
self.change_log_url = "https://ultimaker.com/ultimaker-cura-latest-features"
def __sendCommandToSingleInstance(self):
self._single_instance = SingleInstance(self, self._files_to_open)
@ -311,6 +331,8 @@ class CuraApplication(QtApplication):
# Adds custom property types, settings types, and extra operators (functions) that need to be registered in
# SettingDefinition and SettingFunction.
def __initializeSettingDefinitionsAndFunctions(self):
self._cura_formula_functions = CuraFormulaFunctions(self)
# Need to do this before ContainerRegistry tries to load the machines
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True)
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True)
@ -331,10 +353,10 @@ class CuraApplication(QtApplication):
SettingDefinition.addSettingType("optional_extruder", None, str, None)
SettingDefinition.addSettingType("[int]", None, str, None)
SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues)
SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue)
SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue)
SettingFunction.registerOperator("defaultExtruderPosition", ExtruderManager.getDefaultExtruderPosition)
SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition)
# Adds all resources and container related resources.
def __addAllResourcesAndContainerResources(self) -> None:
@ -365,7 +387,7 @@ class CuraApplication(QtApplication):
# Add empty variant, material and quality containers.
# Since they are empty, they should never be serialized and instead just programmatically created.
# We need them to simplify the switching between materials.
self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container
self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container # type: EmptyInstanceContainer
self._container_registry.addContainer(
cura.Settings.cura_empty_instance_containers.empty_definition_changes_container)
@ -400,7 +422,7 @@ class CuraApplication(QtApplication):
)
# Runs preparations that needs to be done before the starting process.
def startSplashWindowPhase(self):
def startSplashWindowPhase(self) -> None:
super().startSplashWindowPhase()
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
@ -502,19 +524,18 @@ class CuraApplication(QtApplication):
CuraApplication.Created = True
def _onEngineCreated(self):
self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
@pyqtProperty(bool)
def needToShowUserAgreement(self):
def needToShowUserAgreement(self) -> bool:
return self._need_to_show_user_agreement
def setNeedToShowUserAgreement(self, set_value = True):
def setNeedToShowUserAgreement(self, set_value = True) -> None:
self._need_to_show_user_agreement = set_value
# DO NOT call this function to close the application, use checkAndExitApplication() instead which will perform
# pre-exit checks such as checking for in-progress USB printing, etc.
def closeApplication(self):
def closeApplication(self) -> None:
Logger.log("i", "Close application")
main_window = self.getMainWindow()
if main_window is not None:
@ -541,11 +562,11 @@ class CuraApplication(QtApplication):
showConfirmExitDialog = pyqtSignal(str, arguments = ["message"])
def setConfirmExitDialogCallback(self, callback):
def setConfirmExitDialogCallback(self, callback: Callable) -> None:
self._confirm_exit_dialog_callback = callback
@pyqtSlot(bool)
def callConfirmExitDialogCallback(self, yes_or_no: bool):
def callConfirmExitDialogCallback(self, yes_or_no: bool) -> None:
self._confirm_exit_dialog_callback(yes_or_no)
## Signal to connect preferences action in QML
@ -553,9 +574,17 @@ class CuraApplication(QtApplication):
## Show the preferences window
@pyqtSlot()
def showPreferences(self):
def showPreferences(self) -> None:
self.showPreferencesWindow.emit()
@override(Application)
def getGlobalContainerStack(self) -> Optional["GlobalStack"]:
return self._global_container_stack
@override(Application)
def setGlobalContainerStack(self, stack: "GlobalStack") -> None:
super().setGlobalContainerStack(stack)
## A reusable dialogbox
#
showMessageBox = pyqtSignal(str, str, str, str, int, int, arguments = ["title", "text", "informativeText", "detailedText", "buttons", "icon"])
@ -567,7 +596,7 @@ class CuraApplication(QtApplication):
showDiscardOrKeepProfileChanges = pyqtSignal()
def discardOrKeepProfileChanges(self):
def discardOrKeepProfileChanges(self) -> bool:
has_user_interaction = False
choice = self.getPreferences().getValue("cura/choice_on_profile_override")
if choice == "always_discard":
@ -583,7 +612,7 @@ class CuraApplication(QtApplication):
return has_user_interaction
@pyqtSlot(str)
def discardOrKeepProfileChangesClosed(self, option):
def discardOrKeepProfileChangesClosed(self, option: str) -> None:
global_stack = self.getGlobalContainerStack()
if option == "discard":
for extruder in global_stack.extruders.values():
@ -668,11 +697,11 @@ 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")
self._machine_manager = MachineManager(self)
self._machine_manager = MachineManager(self, parent = self)
Logger.log("i", "Initializing container manager")
self._container_manager = ContainerManager(self)
@ -695,10 +724,11 @@ class CuraApplication(QtApplication):
self._print_information = PrintInformation.PrintInformation(self)
self._cura_actions = CuraActions.CuraActions(self)
# Initialize setting visibility presets model
self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self)
default_visibility_profile = self._setting_visibility_presets_model.getItem(0)
self.getPreferences().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
# Initialize setting visibility presets model.
self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self.getPreferences(), parent = self)
# Initialize Cura API
self._cura_API.initialize()
# Detect in which mode to run and execute that mode
if self._is_headless:
@ -798,6 +828,11 @@ class CuraApplication(QtApplication):
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
return self._setting_visibility_presets_model
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
if self._cura_formula_functions is None:
self._cura_formula_functions = CuraFormulaFunctions(self)
return self._cura_formula_functions
def getMachineErrorChecker(self, *args) -> MachineErrorChecker:
return self._machine_error_checker
@ -806,20 +841,20 @@ class CuraApplication(QtApplication):
self._machine_manager = MachineManager(self)
return self._machine_manager
def getExtruderManager(self, *args):
def getExtruderManager(self, *args) -> ExtruderManager:
if self._extruder_manager is None:
self._extruder_manager = ExtruderManager()
return self._extruder_manager
def getVariantManager(self, *args):
def getVariantManager(self, *args) -> VariantManager:
return self._variant_manager
@pyqtSlot(result = QObject)
def getMaterialManager(self, *args):
def getMaterialManager(self, *args) -> "MaterialManager":
return self._material_manager
@pyqtSlot(result = QObject)
def getQualityManager(self, *args):
def getQualityManager(self, *args) -> "QualityManager":
return self._quality_manager
def getObjectsModel(self, *args):
@ -828,23 +863,23 @@ class CuraApplication(QtApplication):
return self._object_manager
@pyqtSlot(result = QObject)
def getMultiBuildPlateModel(self, *args):
def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
if self._multi_build_plate_model is None:
self._multi_build_plate_model = MultiBuildPlateModel(self)
return self._multi_build_plate_model
@pyqtSlot(result = QObject)
def getBuildPlateModel(self, *args):
def getBuildPlateModel(self, *args) -> BuildPlateModel:
if self._build_plate_model is None:
self._build_plate_model = BuildPlateModel(self)
return self._build_plate_model
def getCuraSceneController(self, *args):
def getCuraSceneController(self, *args) -> CuraSceneController:
if self._cura_scene_controller is None:
self._cura_scene_controller = CuraSceneController.createCuraSceneController()
return self._cura_scene_controller
def getSettingInheritanceManager(self, *args):
def getSettingInheritanceManager(self, *args) -> SettingInheritanceManager:
if self._setting_inheritance_manager is None:
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
return self._setting_inheritance_manager
@ -887,6 +922,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.
@ -909,6 +947,8 @@ class CuraApplication(QtApplication):
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
qmlRegisterType(NetworkMJPGImage, "Cura", 1, 0, "NetworkMJPGImage")
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 0, "ObjectsModel", self.getObjectsModel)
qmlRegisterType(BuildPlateModel, "Cura", 1, 0, "BuildPlateModel")
qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
@ -935,6 +975,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")
@ -1574,6 +1617,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()
@ -1588,7 +1636,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)

View file

@ -1,13 +1,13 @@
# Copyright (c) 2016 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.Logger import Logger
from UM.PluginObject import PluginObject
from UM.PluginRegistry import PluginRegistry
from UM.Application import Application
import os
## Machine actions are actions that are added to a specific machine type. Examples of such actions are
@ -19,7 +19,7 @@ class MachineAction(QObject, PluginObject):
## Create a new Machine action.
# \param key unique key of the machine action
# \param label Human readable label used to identify the machine action.
def __init__(self, key, label = ""):
def __init__(self, key: str, label: str = "") -> None:
super().__init__()
self._key = key
self._label = label
@ -30,14 +30,14 @@ class MachineAction(QObject, PluginObject):
labelChanged = pyqtSignal()
onFinished = pyqtSignal()
def getKey(self):
def getKey(self) -> str:
return self._key
@pyqtProperty(str, notify = labelChanged)
def label(self):
def label(self) -> str:
return self._label
def setLabel(self, label):
def setLabel(self, label: str) -> None:
if self._label != label:
self._label = label
self.labelChanged.emit()
@ -46,29 +46,35 @@ class MachineAction(QObject, PluginObject):
# This should not be re-implemented by child classes, instead re-implement _reset.
# /sa _reset
@pyqtSlot()
def reset(self):
def reset(self) -> None:
self._finished = False
self._reset()
## Protected implementation of reset.
# /sa reset()
def _reset(self):
def _reset(self) -> None:
pass
@pyqtSlot()
def setFinished(self):
def setFinished(self) -> None:
self._finished = True
self._reset()
self.onFinished.emit()
@pyqtProperty(bool, notify = onFinished)
def finished(self):
def finished(self) -> bool:
return self._finished
## Protected helper to create a view object based on provided QML.
def _createViewFromQML(self):
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url)
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
def _createViewFromQML(self) -> None:
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
if plugin_path is None:
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
return
path = os.path.join(plugin_path, self._qml_url)
from cura.CuraApplication import CuraApplication
self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
@pyqtProperty(QObject, constant = True)
def displayItem(self):

View file

@ -1,12 +1,18 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING, Optional, List, Set, Dict
from PyQt5.QtCore import QObject
from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as plugin type
from UM.Settings.DefinitionContainer import DefinitionContainer
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
from cura.Settings.GlobalStack import GlobalStack
from .MachineAction import MachineAction
## Raised when trying to add an unknown machine action as a required action
@ -20,46 +26,54 @@ class NotUniqueMachineActionError(Exception):
class MachineActionManager(QObject):
def __init__(self, application, parent = None):
super().__init__(parent)
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
super().__init__(parent = parent)
self._application = application
self._container_registry = self._application.getContainerRegistry()
self._machine_actions = {} # Dict of all known machine actions
self._required_actions = {} # Dict of all required actions by definition ID
self._supported_actions = {} # Dict of all supported actions by definition ID
self._first_start_actions = {} # Dict of all actions that need to be done when first added by definition ID
# Keeps track of which machines have already been processed so we don't do that again.
self._definition_ids_with_default_actions_added = set() # type: Set[str]
# Dict of all known machine actions
self._machine_actions = {} # type: Dict[str, MachineAction]
# Dict of all required actions by definition ID
self._required_actions = {} # type: Dict[str, List[MachineAction]]
# Dict of all supported actions by definition ID
self._supported_actions = {} # type: Dict[str, List[MachineAction]]
# Dict of all actions that need to be done when first added by definition ID
self._first_start_actions = {} # type: Dict[str, List[MachineAction]]
def initialize(self):
container_registry = self._application.getContainerRegistry()
# Add machine_action as plugin type
PluginRegistry.addType("machine_action", self.addMachineAction)
# Ensure that all containers that were registered before creation of this registry are also handled.
# This should not have any effect, but it makes it safer if we ever refactor the order of things.
for container in container_registry.findDefinitionContainers():
self._onContainerAdded(container)
# Adds all default machine actions that are defined in the machine definition for the given machine.
def addDefaultMachineActions(self, global_stack: "GlobalStack") -> None:
definition_id = global_stack.definition.getId()
container_registry.containerAdded.connect(self._onContainerAdded)
if definition_id in self._definition_ids_with_default_actions_added:
Logger.log("i", "Default machine actions have been added for machine definition [%s], do nothing.",
definition_id)
return
def _onContainerAdded(self, container):
## Ensure that the actions are added to this manager
if isinstance(container, DefinitionContainer):
supported_actions = container.getMetaDataEntry("supported_actions", [])
for action in supported_actions:
self.addSupportedAction(container.getId(), action)
supported_actions = global_stack.getMetaDataEntry("supported_actions", [])
for action_key in supported_actions:
self.addSupportedAction(definition_id, action_key)
required_actions = container.getMetaDataEntry("required_actions", [])
for action in required_actions:
self.addRequiredAction(container.getId(), action)
required_actions = global_stack.getMetaDataEntry("required_actions", [])
for action_key in required_actions:
self.addRequiredAction(definition_id, action_key)
first_start_actions = container.getMetaDataEntry("first_start_actions", [])
for action in first_start_actions:
self.addFirstStartAction(container.getId(), action)
first_start_actions = global_stack.getMetaDataEntry("first_start_actions", [])
for action_key in first_start_actions:
self.addFirstStartAction(definition_id, action_key)
self._definition_ids_with_default_actions_added.add(definition_id)
Logger.log("i", "Default machine actions added for machine definition [%s]", definition_id)
## Add a required action to a machine
# Raises an exception when the action is not recognised.
def addRequiredAction(self, definition_id, action_key):
def addRequiredAction(self, definition_id: str, action_key: str) -> None:
if action_key in self._machine_actions:
if definition_id in self._required_actions:
if self._machine_actions[action_key] not in self._required_actions[definition_id]:
@ -70,7 +84,7 @@ class MachineActionManager(QObject):
raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id))
## Add a supported action to a machine.
def addSupportedAction(self, definition_id, action_key):
def addSupportedAction(self, definition_id: str, action_key: str) -> None:
if action_key in self._machine_actions:
if definition_id in self._supported_actions:
if self._machine_actions[action_key] not in self._supported_actions[definition_id]:
@ -81,13 +95,10 @@ class MachineActionManager(QObject):
Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
## Add an action to the first start list of a machine.
def addFirstStartAction(self, definition_id, action_key, index = None):
def addFirstStartAction(self, definition_id: str, action_key: str) -> None:
if action_key in self._machine_actions:
if definition_id in self._first_start_actions:
if index is not None:
self._first_start_actions[definition_id].insert(index, self._machine_actions[action_key])
else:
self._first_start_actions[definition_id].append(self._machine_actions[action_key])
self._first_start_actions[definition_id].append(self._machine_actions[action_key])
else:
self._first_start_actions[definition_id] = [self._machine_actions[action_key]]
else:
@ -95,7 +106,7 @@ class MachineActionManager(QObject):
## Add a (unique) MachineAction
# if the Key of the action is not unique, an exception is raised.
def addMachineAction(self, action):
def addMachineAction(self, action: "MachineAction") -> None:
if action.getKey() not in self._machine_actions:
self._machine_actions[action.getKey()] = action
else:
@ -105,7 +116,7 @@ class MachineActionManager(QObject):
# \param definition_id The ID of the definition you want the supported actions of
# \returns set of supported actions.
@pyqtSlot(str, result = "QVariantList")
def getSupportedActions(self, definition_id):
def getSupportedActions(self, definition_id: str) -> List["MachineAction"]:
if definition_id in self._supported_actions:
return list(self._supported_actions[definition_id])
else:
@ -114,11 +125,11 @@ class MachineActionManager(QObject):
## Get all actions required by given machine
# \param definition_id The ID of the definition you want the required actions of
# \returns set of required actions.
def getRequiredActions(self, definition_id):
def getRequiredActions(self, definition_id: str) -> List["MachineAction"]:
if definition_id in self._required_actions:
return self._required_actions[definition_id]
else:
return set()
return list()
## Get all actions that need to be performed upon first start of a given machine.
# Note that contrary to required / supported actions a list is returned (as it could be required to run the same
@ -126,7 +137,7 @@ class MachineActionManager(QObject):
# \param definition_id The ID of the definition that you want to get the "on added" actions for.
# \returns List of actions.
@pyqtSlot(str, result="QVariantList")
def getFirstStartActions(self, definition_id):
def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
if definition_id in self._first_start_actions:
return self._first_start_actions[definition_id]
else:
@ -134,7 +145,7 @@ class MachineActionManager(QObject):
## Remove Machine action from manager
# \param action to remove
def removeMachineAction(self, action):
def removeMachineAction(self, action: "MachineAction") -> None:
try:
del self._machine_actions[action.getKey()]
except KeyError:
@ -143,7 +154,7 @@ class MachineActionManager(QObject):
## Get MachineAction by key
# \param key String of key to select
# \return Machine action if found, None otherwise
def getMachineAction(self, key):
def getMachineAction(self, key: str) -> Optional["MachineAction"]:
if key in self._machine_actions:
return self._machine_actions[key]
else:

View file

@ -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,29 +21,34 @@ 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.children_map = OrderedDict() #type: OrderedDict[str, Union[QualityGroup, ContainerNode]]
self._metadata = metadata
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
def getMetaDataEntry(self, entry: str, default: Any = None) -> Any:
if self.metadata is None:
if self._metadata is None:
return default
return self.metadata.get(entry, default)
return self._metadata.get(entry, default)
def getMetadata(self) -> Dict[str, Any]:
if self._metadata is None:
return {}
return self._metadata
def getChildNode(self, child_key: str) -> Optional["ContainerNode"]:
return self.children_map.get(child_key)
def getContainer(self) -> Optional["InstanceContainer"]:
if self.metadata is None:
if self._metadata is None:
Logger.log("e", "Cannot get container for a ContainerNode without metadata.")
return None
if self.container is None:
container_id = self.metadata["id"]
if self._container is None:
container_id = self._metadata["id"]
from UM.Settings.ContainerRegistry import ContainerRegistry
container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
if not container_list:
@ -54,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"))

View file

@ -50,7 +50,7 @@ class MachineErrorChecker(QObject):
self._error_check_timer.setInterval(100)
self._error_check_timer.setSingleShot(True)
def initialize(self):
def initialize(self) -> None:
self._error_check_timer.timeout.connect(self._rescheduleCheck)
# Reconnect all signals when the active machine gets changed.
@ -62,7 +62,7 @@ class MachineErrorChecker(QObject):
self._onMachineChanged()
def _onMachineChanged(self):
def _onMachineChanged(self) -> None:
if self._global_stack:
self._global_stack.propertyChanged.disconnect(self.startErrorCheck)
self._global_stack.containersChanged.disconnect(self.startErrorCheck)
@ -94,7 +94,7 @@ class MachineErrorChecker(QObject):
return self._need_to_check or self._check_in_progress
# Starts the error check timer to schedule a new error check.
def startErrorCheck(self, *args):
def startErrorCheck(self, *args) -> None:
if not self._check_in_progress:
self._need_to_check = True
self.needToWaitForResultChanged.emit()
@ -103,7 +103,7 @@ class MachineErrorChecker(QObject):
# This function is called by the timer to reschedule a new error check.
# If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag
# to notify the current check to stop and start a new one.
def _rescheduleCheck(self):
def _rescheduleCheck(self) -> None:
if self._check_in_progress and not self._need_to_check:
self._need_to_check = True
self.needToWaitForResultChanged.emit()
@ -128,7 +128,7 @@ class MachineErrorChecker(QObject):
self._start_time = time.time()
Logger.log("d", "New error check scheduled.")
def _checkStack(self):
def _checkStack(self) -> None:
if self._need_to_check:
Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.")
self._check_in_progress = False
@ -169,7 +169,7 @@ class MachineErrorChecker(QObject):
# Schedule the check for the next key
self._application.callLater(self._checkStack)
def _setResult(self, result: bool):
def _setResult(self, result: bool) -> None:
if result != self._has_errors:
self._has_errors = result
self.hasErrorUpdated.emit()

View file

@ -24,8 +24,8 @@ class MaterialGroup:
def __init__(self, name: str, root_material_node: "MaterialNode") -> None:
self.name = name
self.is_read_only = False
self.root_material_node = root_material_node # type: MaterialNode
self.derived_material_node_list = [] # type: List[MaterialNode]
self.root_material_node = root_material_node # type: MaterialNode
self.derived_material_node_list = [] # type: List[MaterialNode]
def __str__(self) -> str:
return "%s[%s]" % (self.__class__.__name__, self.name)

View file

@ -4,8 +4,7 @@
from collections import defaultdict, OrderedDict
import copy
import uuid
import json
from typing import Dict, Optional, TYPE_CHECKING
from typing import Dict, Optional, TYPE_CHECKING, Any, Set, List, cast, Tuple
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
@ -39,25 +38,35 @@ if TYPE_CHECKING:
#
class MaterialManager(QObject):
materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated.
materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated.
favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed
def __init__(self, container_registry, parent = None):
super().__init__(parent)
self._application = Application.getInstance()
self._container_registry = container_registry # type: ContainerRegistry
self._fallback_materials_map = dict() # material_type -> generic material metadata
self._material_group_map = dict() # root_material_id -> MaterialGroup
self._diameter_machine_nozzle_buildplate_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode)
# Material_type -> generic material metadata
self._fallback_materials_map = dict() # type: Dict[str, Dict[str, Any]]
# Root_material_id -> MaterialGroup
self._material_group_map = dict() # type: Dict[str, MaterialGroup]
# Approximate diameter str
self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
# We're using these two maps to convert between the specific diameter material id and the generic material id
# because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
# i.e. generic_pla -> generic_pla_175
self._material_diameter_map = defaultdict(dict) # root_material_id -> approximate diameter str -> root_material_id for that diameter
self._diameter_material_map = dict() # material id including diameter (generic_pla_175) -> material root id (generic_pla)
# root_material_id -> approximate diameter str -> root_material_id for that diameter
self._material_diameter_map = defaultdict(dict) # type: Dict[str, Dict[str, str]]
# Material id including diameter (generic_pla_175) -> material root id (generic_pla)
self._diameter_material_map = dict() # type: Dict[str, str]
# This is used in Legacy UM3 send material function and the material management page.
self._guid_material_groups_map = defaultdict(list) # GUID -> a list of material_groups
# GUID -> a list of material_groups
self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
# The machine definition ID for the non-machine-specific materials.
# This is used as the last fallback option if the given machine-specific material(s) cannot be found.
@ -76,15 +85,15 @@ class MaterialManager(QObject):
self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
self._favorites = set()
self._favorites = set() # type: Set[str]
def initialize(self):
def initialize(self) -> None:
# Find all materials and put them in a matrix for quick search.
material_metadatas = {metadata["id"]: metadata for metadata in
self._container_registry.findContainersMetadata(type = "material") if
metadata.get("GUID")}
metadata.get("GUID")} # type: Dict[str, Dict[str, Any]]
self._material_group_map = dict()
self._material_group_map = dict() # type: Dict[str, MaterialGroup]
# Map #1
# root_material_id -> MaterialGroup
@ -93,7 +102,7 @@ class MaterialManager(QObject):
if material_id == "empty_material":
continue
root_material_id = material_metadata.get("base_file")
root_material_id = material_metadata.get("base_file", "")
if root_material_id not in self._material_group_map:
self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
@ -109,26 +118,26 @@ class MaterialManager(QObject):
# Map #1.5
# GUID -> material group list
self._guid_material_groups_map = defaultdict(list)
self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
for root_material_id, material_group in self._material_group_map.items():
guid = material_group.root_material_node.metadata["GUID"]
guid = material_group.root_material_node.getMetaDataEntry("GUID", "")
self._guid_material_groups_map[guid].append(material_group)
# Map #2
# Lookup table for material type -> fallback material metadata, only for read-only materials
grouped_by_type_dict = dict()
grouped_by_type_dict = dict() # type: Dict[str, Any]
material_types_without_fallback = set()
for root_material_id, material_node in self._material_group_map.items():
material_type = material_node.root_material_node.metadata["material"]
material_type = material_node.root_material_node.getMetaDataEntry("material", "")
if material_type not in grouped_by_type_dict:
grouped_by_type_dict[material_type] = {"generic": None,
"others": []}
material_types_without_fallback.add(material_type)
brand = material_node.root_material_node.metadata["brand"]
brand = material_node.root_material_node.getMetaDataEntry("brand", "")
if brand.lower() == "generic":
to_add = True
if material_type in grouped_by_type_dict:
diameter = material_node.root_material_node.metadata.get("approximate_diameter")
diameter = material_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
if diameter != self._default_approximate_diameter_for_quality_search:
to_add = False # don't add if it's not the default diameter
@ -137,7 +146,7 @@ class MaterialManager(QObject):
# - if it's in the list, it means that is a new material without fallback
# - if it is not, then it is a custom material with a fallback material (parent)
if material_type in material_types_without_fallback:
grouped_by_type_dict[material_type] = material_node.root_material_node.metadata
grouped_by_type_dict[material_type] = material_node.root_material_node._metadata
material_types_without_fallback.remove(material_type)
# Remove the materials that have no fallback materials
@ -154,15 +163,15 @@ class MaterialManager(QObject):
self._diameter_material_map = dict()
# Group the material IDs by the same name, material, brand, and color but with different diameters.
material_group_dict = dict()
material_group_dict = dict() # type: Dict[Tuple[Any], Dict[str, str]]
keys_to_fetch = ("name", "material", "brand", "color")
for root_material_id, machine_node in self._material_group_map.items():
root_material_metadata = machine_node.root_material_node.metadata
root_material_metadata = machine_node.root_material_node._metadata
key_data = []
key_data_list = [] # type: List[Any]
for key in keys_to_fetch:
key_data.append(root_material_metadata.get(key))
key_data = tuple(key_data)
key_data_list.append(machine_node.root_material_node.getMetaDataEntry(key))
key_data = cast(Tuple[Any], tuple(key_data_list)) # type: Tuple[Any]
# If the key_data doesn't exist, it doesn't matter if the material is read only...
if key_data not in material_group_dict:
@ -171,8 +180,8 @@ class MaterialManager(QObject):
# ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it
if not machine_node.is_read_only:
continue
approximate_diameter = root_material_metadata.get("approximate_diameter")
material_group_dict[key_data][approximate_diameter] = root_material_metadata["id"]
approximate_diameter = machine_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
material_group_dict[key_data][approximate_diameter] = machine_node.root_material_node.getMetaDataEntry("id", "")
# Map [root_material_id][diameter] -> root_material_id for this diameter
for data_dict in material_group_dict.values():
@ -191,7 +200,7 @@ class MaterialManager(QObject):
# Map #4
# "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
self._diameter_machine_nozzle_buildplate_material_map = dict()
self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
for material_metadata in material_metadatas.values():
self.__addMaterialMetadataIntoLookupTree(material_metadata)
@ -201,7 +210,7 @@ class MaterialManager(QObject):
self.materialsUpdated.emit()
def __addMaterialMetadataIntoLookupTree(self, material_metadata: dict) -> None:
def __addMaterialMetadataIntoLookupTree(self, material_metadata: Dict[str, Any]) -> None:
material_id = material_metadata["id"]
# We don't store empty material in the lookup tables
@ -288,9 +297,9 @@ class MaterialManager(QObject):
return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id)
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
return self._diameter_material_map.get(root_material_id)
return self._diameter_material_map.get(root_material_id, "")
def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]:
def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
return self._guid_material_groups_map.get(guid)
#
@ -328,6 +337,7 @@ class MaterialManager(QObject):
machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", [])
material_id_metadata_dict = dict() # type: Dict[str, MaterialNode]
excluded_materials = set()
for current_node in nodes_to_check:
if current_node is None:
continue
@ -336,25 +346,27 @@ class MaterialManager(QObject):
# Do not exclude other materials that are of the same type.
for material_id, node in current_node.material_map.items():
if material_id in machine_exclude_materials:
Logger.log("d", "Exclude material [%s] for machine [%s]",
material_id, machine_definition.getId())
excluded_materials.add(material_id)
continue
if material_id not in material_id_metadata_dict:
material_id_metadata_dict[material_id] = node
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
#
# A convenience function to get available materials for the given machine with the extruder position.
#
def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
extruder_stack: "ExtruderStack") -> Optional[dict]:
extruder_stack: "ExtruderStack") -> Optional[Dict[str, MaterialNode]]:
buildplate_name = machine.getBuildplateName()
nozzle_name = None
if extruder_stack.variant.getId() != "empty_variant":
nozzle_name = extruder_stack.variant.getName()
diameter = extruder_stack.approximateMaterialDiameter
diameter = extruder_stack.getApproximateMaterialDiameter()
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter)
@ -366,7 +378,7 @@ class MaterialManager(QObject):
# 2. cannot find any material InstanceContainers with the given settings.
#
def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str],
buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["InstanceContainer"]:
buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["MaterialNode"]:
# round the diameter to get the approximate diameter
rounded_diameter = str(round(diameter))
if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
@ -375,7 +387,7 @@ class MaterialManager(QObject):
return None
# If there are nozzle materials, get the nozzle-specific material
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter]
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] # type: Dict[str, MaterialNode]
machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
nozzle_node = None
buildplate_node = None
@ -424,7 +436,7 @@ class MaterialManager(QObject):
# Look at the guid to material dictionary
root_material_id = None
for material_group in self._guid_material_groups_map[material_guid]:
root_material_id = material_group.root_material_node.metadata["id"]
root_material_id = cast(str, material_group.root_material_node.getMetaDataEntry("id", ""))
break
if not root_material_id:
@ -435,6 +447,28 @@ class MaterialManager(QObject):
material_diameter, root_material_id)
return node
# There are 2 ways to get fallback materials;
# - A fallback by type (@sa getFallbackMaterialIdByMaterialType), which adds the generic version of this material
# - A fallback by GUID; If a material has been duplicated, it should also check if the original materials do have
# a GUID. This should only be done if the material itself does not have a quality just yet.
def getFallBackMaterialIdsByMaterial(self, material: "InstanceContainer") -> List[str]:
results = [] # type: List[str]
material_groups = self.getMaterialGroupListByGUID(material.getMetaDataEntry("GUID"))
for material_group in material_groups: # type: ignore
if material_group.name != material.getId():
# If the material in the group is read only, put it at the front of the list (since that is the most
# likely one to get a result)
if material_group.is_read_only:
results.insert(0, material_group.name)
else:
results.append(material_group.name)
fallback = self.getFallbackMaterialIdByMaterialType(material.getMetaDataEntry("material"))
if fallback is not None:
results.append(fallback)
return results
#
# Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
# For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
@ -467,12 +501,22 @@ class MaterialManager(QObject):
buildplate_name = global_stack.getBuildplateName()
machine_definition = global_stack.definition
if extruder_definition is None:
extruder_definition = global_stack.extruders[position].definition
if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
# At this point the extruder_definition is not None
material_diameter = extruder_definition.getProperty("material_diameter", "value")
# The extruder-compatible material diameter in the extruder definition may not be the correct value because
# the user can change it in the definition_changes container.
if extruder_definition is None:
extruder_stack_or_definition = global_stack.extruders[position]
is_extruder_stack = True
else:
extruder_stack_or_definition = extruder_definition
is_extruder_stack = False
if extruder_stack_or_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
if is_extruder_stack:
material_diameter = extruder_stack_or_definition.getCompatibleMaterialDiameter()
else:
material_diameter = extruder_stack_or_definition.getProperty("material_diameter", "value")
if isinstance(material_diameter, SettingFunction):
material_diameter = material_diameter(global_stack)
approximate_material_diameter = str(round(material_diameter))
@ -500,7 +544,7 @@ class MaterialManager(QObject):
# Sets the new name for the given material.
#
@pyqtSlot("QVariant", str)
def setMaterialName(self, material_node: "MaterialNode", name: str):
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
root_material_id = material_node.getMetaDataEntry("base_file")
if root_material_id is None:
return
@ -518,7 +562,7 @@ class MaterialManager(QObject):
# Removes the given material.
#
@pyqtSlot("QVariant")
def removeMaterial(self, material_node: "MaterialNode"):
def removeMaterial(self, material_node: "MaterialNode") -> None:
root_material_id = material_node.getMetaDataEntry("base_file")
if root_material_id is not None:
self.removeMaterialByRootId(root_material_id)
@ -528,8 +572,8 @@ class MaterialManager(QObject):
# Returns the root material ID of the duplicated material if successful.
#
@pyqtSlot("QVariant", result = str)
def duplicateMaterial(self, material_node, new_base_id = None, new_metadata = None) -> Optional[str]:
root_material_id = material_node.metadata["base_file"]
def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
root_material_id = cast(str, material_node.getMetaDataEntry("base_file", ""))
material_group = self.getMaterialGroup(root_material_id)
if not material_group:
@ -580,11 +624,16 @@ class MaterialManager(QObject):
for container_to_add in new_containers:
container_to_add.setDirty(True)
self._container_registry.addContainer(container_to_add)
# if the duplicated material was favorite then the new material should also be added to favorite.
if root_material_id in self.getFavorites():
self.addFavorite(new_base_id)
return new_base_id
#
# Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID.
#
# Returns the ID of the newly created material.
@pyqtSlot(result = str)
def createMaterial(self) -> str:
from UM.i18n import i18nCatalog
@ -617,7 +666,7 @@ class MaterialManager(QObject):
return new_id
@pyqtSlot(str)
def addFavorite(self, root_material_id: str):
def addFavorite(self, root_material_id: str) -> None:
self._favorites.add(root_material_id)
self.materialsUpdated.emit()
@ -626,7 +675,7 @@ class MaterialManager(QObject):
self._application.saveSettings()
@pyqtSlot(str)
def removeFavorite(self, root_material_id: str):
def removeFavorite(self, root_material_id: str) -> None:
self._favorites.remove(root_material_id)
self.materialsUpdated.emit()

View file

@ -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
from typing import Optional, Dict, Any
from collections import OrderedDict
from .ContainerNode import ContainerNode
@ -14,6 +14,12 @@ from .ContainerNode import ContainerNode
class MaterialNode(ContainerNode):
__slots__ = ("material_map", "children_map")
def __init__(self, metadata: Optional[dict] = None) -> None:
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
super().__init__(metadata = metadata)
self.material_map = {} # type: Dict[str, MaterialNode] # material_root_id -> material_node
# We overide this as we want to indicate that MaterialNodes can only contain other material nodes.
self.children_map = OrderedDict() # type: OrderedDict[str, "MaterialNode"]
def getChildNode(self, child_key: str) -> Optional["MaterialNode"]:
return self.children_map.get(child_key)

View file

@ -64,9 +64,11 @@ class BaseMaterialsModel(ListModel):
if self._extruder_stack is not None:
self._extruder_stack.pyqtContainersChanged.disconnect(self._update)
self._extruder_stack.approximateMaterialDiameterChanged.disconnect(self._update)
self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
if self._extruder_stack is not None:
self._extruder_stack.pyqtContainersChanged.connect(self._update)
self._extruder_stack.approximateMaterialDiameterChanged.connect(self._update)
# Force update the model when the extruder stack changes
self._update()
@ -110,7 +112,7 @@ class BaseMaterialsModel(ListModel):
## This is another convenience function which is shared by all material
# models so it's put here to avoid having so much duplicated code.
def _createMaterialItem(self, root_material_id, container_node):
metadata = container_node.metadata
metadata = container_node.getMetadata()
item = {
"root_material_id": root_material_id,
"id": metadata["id"],

View file

@ -23,7 +23,7 @@ class FavoriteMaterialsModel(BaseMaterialsModel):
item_list = []
for root_material_id, container_node in self._available_materials.items():
metadata = container_node.metadata
metadata = container_node.getMetadata()
# Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)):

View file

@ -23,7 +23,7 @@ class GenericMaterialsModel(BaseMaterialsModel):
item_list = []
for root_material_id, container_node in self._available_materials.items():
metadata = container_node.metadata
metadata = container_node.getMetadata()
# Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)):

View file

@ -42,21 +42,19 @@ class MaterialBrandsModel(BaseMaterialsModel):
# Part 1: Generate the entire tree of brands -> material types -> spcific materials
for root_material_id, container_node in self._available_materials.items():
metadata = container_node.metadata
# Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)):
if bool(container_node.getMetaDataEntry("removed", False)):
continue
# Add brands we haven't seen yet to the dict, skipping generics
brand = metadata["brand"]
brand = container_node.getMetaDataEntry("brand", "")
if brand.lower() == "generic":
continue
if brand not in brand_group_dict:
brand_group_dict[brand] = {}
# Add material types we haven't seen yet to the dict
material_type = metadata["material"]
material_type = container_node.getMetaDataEntry("material", "")
if material_type not in brand_group_dict[brand]:
brand_group_dict[brand][material_type] = []

View file

@ -1,135 +1,109 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
import os
import urllib.parse
from configparser import ConfigParser
from typing import Optional, List
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from UM.Application import Application
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from UM.Resources import Resources
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
from UM.i18n import i18nCatalog
from cura.Settings.SettingVisibilityPreset import SettingVisibilityPreset
catalog = i18nCatalog("cura")
class SettingVisibilityPresetsModel(ListModel):
IdRole = Qt.UserRole + 1
NameRole = Qt.UserRole + 2
SettingsRole = Qt.UserRole + 3
class SettingVisibilityPresetsModel(QObject):
onItemsChanged = pyqtSignal()
activePresetChanged = pyqtSignal()
def __init__(self, parent = None):
def __init__(self, preferences, parent = None):
super().__init__(parent)
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.SettingsRole, "settings")
self._items = [] # type: List[SettingVisibilityPreset]
self._populate()
basic_item = self.items[1]
basic_visibile_settings = ";".join(basic_item["settings"])
self._preferences = Application.getInstance().getPreferences()
basic_item = self.getVisibilityPresetById("basic")
basic_visibile_settings = ";".join(basic_item.settings)
self._preferences = preferences
# Preference to store which preset is currently selected
self._preferences.addPreference("cura/active_setting_visibility_preset", "basic")
# Preference that stores the "custom" set so it can always be restored (even after a restart)
self._preferences.addPreference("cura/custom_visible_settings", basic_visibile_settings)
self._preferences.preferenceChanged.connect(self._onPreferencesChanged)
self._active_preset_item = self._getItem(self._preferences.getValue("cura/active_setting_visibility_preset"))
self._active_preset_item = self.getVisibilityPresetById(self._preferences.getValue("cura/active_setting_visibility_preset"))
# Initialize visible settings if it is not done yet
visible_settings = self._preferences.getValue("general/visible_settings")
if not visible_settings:
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item["settings"]))
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item.settings))
else:
self._onPreferencesChanged("general/visible_settings")
self.activePresetChanged.emit()
def _getItem(self, item_id: str) -> Optional[dict]:
def getVisibilityPresetById(self, item_id: str) -> Optional[SettingVisibilityPreset]:
result = None
for item in self.items:
if item["id"] == item_id:
for item in self._items:
if item.presetId == item_id:
result = item
break
return result
def _populate(self):
def _populate(self) -> None:
from cura.CuraApplication import CuraApplication
items = []
items = [] # type: List[SettingVisibilityPreset]
custom_preset = SettingVisibilityPreset(preset_id="custom", name ="Custom selection", weight = -100)
items.append(custom_preset)
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
setting_visibility_preset = SettingVisibilityPreset()
try:
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
except MimeTypeNotFoundError:
Logger.log("e", "Could not determine mime type of file %s", file_path)
continue
item_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_path)))
if not os.path.isfile(file_path):
Logger.log("e", "[%s] is not a file", file_path)
continue
parser = ConfigParser(allow_no_value = True) # accept options without any value,
try:
parser.read([file_path])
if not parser.has_option("general", "name") or not parser.has_option("general", "weight"):
continue
settings = []
for section in parser.sections():
if section == 'general':
continue
settings.append(section)
for option in parser[section].keys():
settings.append(option)
items.append({
"id": item_id,
"name": catalog.i18nc("@action:inmenu", parser["general"]["name"]),
"weight": parser["general"]["weight"],
"settings": settings,
})
setting_visibility_preset.loadFromFile(file_path)
except Exception:
Logger.logException("e", "Failed to load setting preset %s", file_path)
items.sort(key = lambda k: (int(k["weight"]), k["id"]))
# Put "custom" at the top
items.insert(0, {"id": "custom",
"name": "Custom selection",
"weight": -100,
"settings": []})
items.append(setting_visibility_preset)
# Sort them on weight (and if that fails, use ID)
items.sort(key = lambda k: (int(k.weight), k.presetId))
self.setItems(items)
@pyqtProperty("QVariantList", notify = onItemsChanged)
def items(self):
return self._items
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
if self._items != items:
self._items = items
self.onItemsChanged.emit()
@pyqtSlot(str)
def setActivePreset(self, preset_id: str):
if preset_id == self._active_preset_item["id"]:
def setActivePreset(self, preset_id: str) -> None:
if preset_id == self._active_preset_item.presetId:
Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
return
preset_item = None
for item in self.items:
if item["id"] == preset_id:
preset_item = item
break
preset_item = self.getVisibilityPresetById(preset_id)
if preset_item is None:
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
return
need_to_save_to_custom = self._active_preset_item["id"] == "custom" and preset_id != "custom"
need_to_save_to_custom = self._active_preset_item.presetId == "custom" and preset_id != "custom"
if need_to_save_to_custom:
# Save the current visibility settings to custom
current_visibility_string = self._preferences.getValue("general/visible_settings")
if current_visibility_string:
self._preferences.setValue("cura/custom_visible_settings", current_visibility_string)
new_visibility_string = ";".join(preset_item["settings"])
new_visibility_string = ";".join(preset_item.settings)
if preset_id == "custom":
# Get settings from the stored custom data
new_visibility_string = self._preferences.getValue("cura/custom_visible_settings")
@ -141,13 +115,11 @@ class SettingVisibilityPresetsModel(ListModel):
self._active_preset_item = preset_item
self.activePresetChanged.emit()
activePresetChanged = pyqtSignal()
@pyqtProperty(str, notify = activePresetChanged)
def activePreset(self) -> str:
return self._active_preset_item["id"]
return self._active_preset_item.presetId
def _onPreferencesChanged(self, name: str):
def _onPreferencesChanged(self, name: str) -> None:
if name != "general/visible_settings":
return
@ -158,25 +130,26 @@ class SettingVisibilityPresetsModel(ListModel):
visibility_set = set(visibility_string.split(";"))
matching_preset_item = None
for item in self.items:
if item["id"] == "custom":
for item in self._items:
if item.presetId == "custom":
continue
if set(item["settings"]) == visibility_set:
if set(item.settings) == visibility_set:
matching_preset_item = item
break
item_to_set = self._active_preset_item
if matching_preset_item is None:
# The new visibility setup is "custom" should be custom
if self._active_preset_item["id"] == "custom":
if self._active_preset_item is None or self._active_preset_item.presetId == "custom":
# We are already in custom, just save the settings
self._preferences.setValue("cura/custom_visible_settings", visibility_string)
else:
item_to_set = self.items[0] # 0 is custom
# We need to move to custom preset.
item_to_set = self.getVisibilityPresetById("custom")
else:
item_to_set = matching_preset_item
if self._active_preset_item is None or self._active_preset_item["id"] != item_to_set["id"]:
if self._active_preset_item is None or self._active_preset_item.presetId != item_to_set.presetId:
self._active_preset_item = item_to_set
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item["id"])
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item.presetId)
self.activePresetChanged.emit()

View file

@ -17,16 +17,16 @@ class QualityChangesGroup(QualityGroup):
super().__init__(name, quality_type, parent)
self._container_registry = Application.getInstance().getContainerRegistry()
def addNode(self, node: "QualityNode"):
def addNode(self, node: "QualityNode") -> None:
extruder_position = node.getMetaDataEntry("position")
if extruder_position is None and self.node_for_global is not None or extruder_position in self.nodes_for_extruders: #We would be overwriting another node.
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:

View file

@ -6,6 +6,7 @@ from typing import Dict, Optional, List, Set
from PyQt5.QtCore import QObject, pyqtSlot
from cura.Machines.ContainerNode import ContainerNode
#
# A QualityGroup represents a group of containers that must be applied to each ContainerStack when it's used.
# Some concrete examples are Quality and QualityChanges: when we select quality type "normal", this quality type
@ -34,7 +35,7 @@ class QualityGroup(QObject):
return self.name
def getAllKeys(self) -> Set[str]:
result = set() #type: Set[str]
result = set() # type: Set[str]
for node in [self.node_for_global] + list(self.nodes_for_extruders.values()):
if node is None:
continue

View file

@ -1,11 +1,10 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING, Optional, cast
from typing import TYPE_CHECKING, Optional, cast, Dict, List, Set
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
@ -20,6 +19,7 @@ if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer
from cura.Settings.GlobalStack import GlobalStack
from .QualityChangesGroup import QualityChangesGroup
from cura.CuraApplication import CuraApplication
#
@ -36,17 +36,20 @@ class QualityManager(QObject):
qualitiesUpdated = pyqtSignal()
def __init__(self, container_registry, parent = None):
def __init__(self, application: "CuraApplication", parent = None) -> None:
super().__init__(parent)
self._application = Application.getInstance()
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
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # for quality lookup
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup
# For quality lookup
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # type: Dict[str, QualityNode]
# For quality_changes lookup
self._machine_quality_type_to_quality_changes_dict = {} # type: Dict[str, QualityNode]
self._default_machine_definition_id = "fdmprinter"
@ -62,7 +65,7 @@ class QualityManager(QObject):
self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._updateMaps)
def initialize(self):
def initialize(self) -> None:
# Initialize the lookup tree for quality profiles with following structure:
# <machine> -> <nozzle> -> <buildplate> -> <material>
# <machine> -> <material>
@ -133,13 +136,13 @@ class QualityManager(QObject):
Logger.log("d", "Lookup tables updated.")
self.qualitiesUpdated.emit()
def _updateMaps(self):
def _updateMaps(self) -> None:
self.initialize()
def _onContainerMetadataChanged(self, container):
def _onContainerMetadataChanged(self, container: InstanceContainer) -> None:
self._onContainerChanged(container)
def _onContainerChanged(self, container):
def _onContainerChanged(self, container: InstanceContainer) -> None:
container_type = container.getMetaDataEntry("type")
if container_type not in ("quality", "quality_changes"):
return
@ -148,7 +151,7 @@ class QualityManager(QObject):
self._update_timer.start()
# Updates the given quality groups' availabilities according to which extruders are being used/ enabled.
def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list):
def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list) -> None:
used_extruders = set()
for i in range(machine.getProperty("machine_extruder_count", "value")):
if str(i) in machine.extruders and machine.extruders[str(i)].isEnabled:
@ -196,7 +199,7 @@ class QualityManager(QObject):
# Whether a QualityGroup is available can be unknown via the field QualityGroup.is_available.
# For more details, see QualityGroup.
#
def getQualityGroups(self, machine: "GlobalStack") -> dict:
def getQualityGroups(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
# This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
@ -214,19 +217,24 @@ class QualityManager(QObject):
has_extruder_specific_qualities = True
default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(self._default_machine_definition_id)
nodes_to_check = [machine_node, default_machine_node]
nodes_to_check = [] # type: List[QualityNode]
if machine_node is not None:
nodes_to_check.append(machine_node)
if default_machine_node is not None:
nodes_to_check.append(default_machine_node)
# Iterate over all quality_types in the machine node
quality_group_dict = {}
for node in nodes_to_check:
if node and node.quality_type_map:
quality_node = list(node.quality_type_map.values())[0]
is_global_quality = parseBool(quality_node.metadata.get("global_quality", False))
is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False))
if not is_global_quality:
continue
for quality_type, quality_node in node.quality_type_map.items():
quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group.node_for_global = quality_node
quality_group_dict[quality_type] = quality_group
break
@ -251,11 +259,15 @@ class QualityManager(QObject):
root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
root_material_id_list.append(root_material_id)
# Also try to get the fallback material
material_type = extruder.material.getMetaDataEntry("material")
fallback_root_material_id = self._material_manager.getFallbackMaterialIdByMaterialType(material_type)
if fallback_root_material_id:
root_material_id_list.append(fallback_root_material_id)
# Also try to get the fallback materials
fallback_ids = self._material_manager.getFallBackMaterialIdsByMaterial(extruder.material)
if fallback_ids:
root_material_id_list.extend(fallback_ids)
# Weed out duplicates while preserving the order.
seen = set() # type: Set[str]
root_material_id_list = [x for x in root_material_id_list if x not in seen and not seen.add(x)] # type: ignore
# Here we construct a list of nodes we want to look for qualities with the highest priority first.
# The use case is that, when we look for qualities for a machine, we first want to search in the following
@ -273,13 +285,16 @@ class QualityManager(QObject):
# Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into
# the list with priorities as the order. Later, we just need to loop over each node in this list and fetch
# qualities from there.
node_info_list_0 = [nozzle_name, buildplate_name, root_material_id]
node_info_list_0 = [nozzle_name, buildplate_name, root_material_id] # type: List[Optional[str]]
nodes_to_check = []
# This function tries to recursively find the deepest (the most specific) branch and add those nodes to
# the search list in the order described above. So, by iterating over that search node list, we first look
# in the more specific branches and then the less specific (generic) ones.
def addNodesToCheck(node, nodes_to_check_list, node_info_list, node_info_idx):
def addNodesToCheck(node: Optional[QualityNode], nodes_to_check_list: List[QualityNode], node_info_list, node_info_idx: int) -> None:
if node is None:
return
if node_info_idx < len(node_info_list):
node_name = node_info_list[node_info_idx]
if node_name is not None:
@ -300,9 +315,10 @@ class QualityManager(QObject):
# The last fall back will be the global qualities (either from the machine-specific node or the generic
# node), but we only use one. For details see the overview comments above.
if machine_node.quality_type_map:
if machine_node is not None and machine_node.quality_type_map:
nodes_to_check += [machine_node]
else:
elif default_machine_node is not None:
nodes_to_check += [default_machine_node]
for node_idx, node in enumerate(nodes_to_check):
@ -310,13 +326,13 @@ class QualityManager(QObject):
if has_extruder_specific_qualities:
# Only include variant qualities; skip non global qualities
quality_node = list(node.quality_type_map.values())[0]
is_global_quality = parseBool(quality_node.metadata.get("global_quality", False))
is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False))
if is_global_quality:
continue
for quality_type, quality_node in node.quality_type_map.items():
if quality_type not in quality_group_dict:
quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group_dict[quality_type] = quality_group
quality_group = quality_group_dict[quality_type]
@ -334,7 +350,7 @@ class QualityManager(QObject):
return quality_group_dict
def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> dict:
def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
# To find the quality container for the GlobalStack, check in the following fall-back manner:
@ -350,7 +366,7 @@ class QualityManager(QObject):
for node in nodes_to_check:
if node and node.quality_type_map:
for quality_type, quality_node in node.quality_type_map.items():
quality_group = QualityGroup(quality_node.metadata["name"], quality_type)
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group.node_for_global = quality_node
quality_group_dict[quality_type] = quality_group
break
@ -372,7 +388,7 @@ class QualityManager(QObject):
# Remove the given quality changes group.
#
@pyqtSlot(QObject)
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup"):
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
Logger.log("i", "Removing quality changes group [%s]", quality_changes_group.name)
removed_quality_changes_ids = set()
for node in quality_changes_group.getAllNodes():
@ -415,7 +431,7 @@ class QualityManager(QObject):
# Duplicates the given quality.
#
@pyqtSlot(str, "QVariantMap")
def duplicateQualityChanges(self, quality_changes_name, quality_model_item):
def duplicateQualityChanges(self, quality_changes_name: str, quality_model_item) -> None:
global_stack = self._application.getGlobalContainerStack()
if not global_stack:
Logger.log("i", "No active global stack, cannot duplicate quality changes.")
@ -443,8 +459,8 @@ class QualityManager(QObject):
# the user containers in each stack. These then replace the quality_changes containers in the
# stack and clear the user settings.
@pyqtSlot(str)
def createQualityChanges(self, base_name):
machine_manager = Application.getInstance().getMachineManager()
def createQualityChanges(self, base_name: str) -> None:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
if not global_stack:

View file

@ -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,18 +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 addQualityMetadata(self, quality_type: str, metadata: dict):
def getChildNode(self, child_key: str) -> Optional["QualityNode"]:
return self.children_map.get(child_key)
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]

View file

@ -2,7 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
from collections import OrderedDict
from typing import Optional, TYPE_CHECKING
from typing import Optional, TYPE_CHECKING, Dict
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Logger import Logger
@ -36,11 +36,11 @@ if TYPE_CHECKING:
#
class VariantManager:
def __init__(self, container_registry):
self._container_registry = container_registry # type: ContainerRegistry
def __init__(self, container_registry: ContainerRegistry) -> None:
self._container_registry = container_registry
self._machine_to_variant_dict_map = dict() # <machine_type> -> <variant_dict>
self._machine_to_buildplate_dict_map = dict()
self._machine_to_variant_dict_map = dict() # type: Dict[str, Dict["VariantType", Dict[str, ContainerNode]]]
self._machine_to_buildplate_dict_map = dict() # type: Dict[str, Dict[str, ContainerNode]]
self._exclude_variant_id_list = ["empty_variant"]
@ -48,7 +48,7 @@ class VariantManager:
# Initializes the VariantManager including:
# - initializing the variant lookup table based on the metadata in ContainerRegistry.
#
def initialize(self):
def initialize(self) -> None:
self._machine_to_variant_dict_map = OrderedDict()
self._machine_to_buildplate_dict_map = OrderedDict()
@ -106,10 +106,10 @@ class VariantManager:
variant_node = variant_dict[variant_name]
break
return variant_node
return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name)
def getVariantNodes(self, machine: "GlobalStack",
variant_type: Optional["VariantType"] = None) -> dict:
def getVariantNodes(self, machine: "GlobalStack", variant_type: "VariantType") -> Dict[str, ContainerNode]:
machine_definition_id = machine.definition.getId()
return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {})

View 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()

View 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]

View 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

View 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)

View 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
View 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
View file

@ -0,0 +1,2 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.

View file

@ -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)

View file

@ -6,73 +6,59 @@ import math
import os
import unicodedata
import re # To create abbreviations for printer names.
from typing import Dict
from typing import Dict, List, Optional
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
from UM.i18n import i18nCatalog
from UM.MimeTypeDatabase import MimeTypeDatabase
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
catalog = i18nCatalog("cura")
## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
#
# This class contains all the logic relating to calculation and slicing for the
# time/quality slider concept. It is a rather tricky combination of event handling
# and state management. The logic behind this is as follows:
#
# - A scene change or setting change event happens.
# We track what the source was of the change, either a scene change, a setting change, an active machine change or something else.
# - This triggers a new slice with the current settings - this is the "current settings pass".
# - When the slice is done, we update the current print time and material amount.
# - If the source of the slice was not a Setting change, we start the second slice pass, the "low quality settings pass". Otherwise we stop here.
# - When that is done, we update the minimum print time and start the final slice pass, the "Extra Fine settings pass".
# - When the Extra Fine pass is done, we update the maximum print time.
## A class for processing and the print times per build plate as well as managing the job name
#
# This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
# This job name is requested by the JobSpecs qml file.
class PrintInformation(QObject):
class SlicePass:
CurrentSettings = 1
LowQualitySettings = 2
HighQualitySettings = 3
class SliceReason:
SceneChanged = 1
SettingChanged = 2
ActiveMachineChanged = 3
Other = 4
UNTITLED_JOB_NAME = "Untitled"
def __init__(self, application, parent = None):
def __init__(self, application: "CuraApplication", parent = None) -> None:
super().__init__(parent)
self._application = application
self.initializeCuraMessagePrintTimeProperties()
self._material_lengths = {} # indexed by build plate number
self._material_weights = {}
self._material_costs = {}
self._material_names = {}
# Indexed by build plate number
self._material_lengths = {} # type: Dict[int, List[float]]
self._material_weights = {} # type: Dict[int, List[float]]
self._material_costs = {} # type: Dict[int, List[float]]
self._material_names = {} # type: Dict[int, List[str]]
self._pre_sliced = False
self._backend = self._application.getBackend()
if self._backend:
self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
self._is_user_specified_job_name = False
self._base_name = ""
self._abbr_machine = ""
self._job_name = ""
self._project_name = ""
self._active_build_plate = 0
self._initVariablesWithBuildPlate(self._active_build_plate)
self._initVariablesByBuildPlate(self._active_build_plate)
self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
@ -80,19 +66,17 @@ class PrintInformation(QObject):
self._application.globalContainerStackChanged.connect(self.setToZeroPrintInformation)
self._application.fileLoaded.connect(self.setBaseName)
self._application.workspaceLoaded.connect(self.setProjectName)
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged)
self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged)
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
self._onActiveMaterialsChanged()
self._material_amounts = []
self._material_amounts = [] # type: List[float]
# Crate cura message translations and using translation keys initialize empty time Duration object for total time
# and time for each feature
def initializeCuraMessagePrintTimeProperties(self):
self._current_print_time = {} # Duration(None, self)
def initializeCuraMessagePrintTimeProperties(self) -> None:
self._current_print_time = {} # type: Dict[int, Duration]
self._print_time_message_translations = {
"inset_0": catalog.i18nc("@tooltip", "Outer Wall"),
@ -108,17 +92,17 @@ class PrintInformation(QObject):
"none": catalog.i18nc("@tooltip", "Other")
}
self._print_time_message_values = {}
self._print_times_per_feature = {} # type: Dict[int, Dict[str, Duration]]
def _initPrintTimeMessageValues(self, build_plate_number):
def _initPrintTimesPerFeature(self, build_plate_number: int) -> None:
# Full fill message values using keys from _print_time_message_translations
self._print_time_message_values[build_plate_number] = {}
self._print_times_per_feature[build_plate_number] = {}
for key in self._print_time_message_translations.keys():
self._print_time_message_values[build_plate_number][key] = Duration(None, self)
self._print_times_per_feature[build_plate_number][key] = Duration(None, self)
def _initVariablesWithBuildPlate(self, build_plate_number):
if build_plate_number not in self._print_time_message_values:
self._initPrintTimeMessageValues(build_plate_number)
def _initVariablesByBuildPlate(self, build_plate_number: int) -> None:
if build_plate_number not in self._print_times_per_feature:
self._initPrintTimesPerFeature(build_plate_number)
if self._active_build_plate not in self._material_lengths:
self._material_lengths[self._active_build_plate] = []
if self._active_build_plate not in self._material_weights:
@ -128,23 +112,24 @@ class PrintInformation(QObject):
if self._active_build_plate not in self._material_names:
self._material_names[self._active_build_plate] = []
if self._active_build_plate not in self._current_print_time:
self._current_print_time[self._active_build_plate] = Duration(None, self)
self._current_print_time[self._active_build_plate] = Duration(parent = self)
currentPrintTimeChanged = pyqtSignal()
preSlicedChanged = pyqtSignal()
@pyqtProperty(bool, notify=preSlicedChanged)
def preSliced(self):
def preSliced(self) -> bool:
return self._pre_sliced
def setPreSliced(self, pre_sliced):
self._pre_sliced = pre_sliced
self._updateJobName()
self.preSlicedChanged.emit()
def setPreSliced(self, pre_sliced: bool) -> None:
if self._pre_sliced != pre_sliced:
self._pre_sliced = pre_sliced
self._updateJobName()
self.preSlicedChanged.emit()
@pyqtProperty(Duration, notify = currentPrintTimeChanged)
def currentPrintTime(self):
def currentPrintTime(self) -> Duration:
return self._current_print_time[self._active_build_plate]
materialLengthsChanged = pyqtSignal()
@ -171,36 +156,41 @@ class PrintInformation(QObject):
def materialNames(self):
return self._material_names[self._active_build_plate]
def printTimes(self):
return self._print_time_message_values[self._active_build_plate]
# Get all print times (by feature) of the active buildplate.
def printTimes(self) -> Dict[str, Duration]:
return self._print_times_per_feature[self._active_build_plate]
def _onPrintDurationMessage(self, build_plate_number, print_time: Dict[str, int], material_amounts: list):
self._updateTotalPrintTimePerFeature(build_plate_number, print_time)
def _onPrintDurationMessage(self, build_plate_number: int, print_times_per_feature: Dict[str, int], material_amounts: List[float]) -> None:
self._updateTotalPrintTimePerFeature(build_plate_number, print_times_per_feature)
self.currentPrintTimeChanged.emit()
self._material_amounts = material_amounts
self._calculateInformation(build_plate_number)
def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time: Dict[str, int]):
def _updateTotalPrintTimePerFeature(self, build_plate_number: int, print_times_per_feature: Dict[str, int]) -> None:
total_estimated_time = 0
if build_plate_number not in self._print_time_message_values:
self._initPrintTimeMessageValues(build_plate_number)
if build_plate_number not in self._print_times_per_feature:
self._initPrintTimesPerFeature(build_plate_number)
for feature, time in print_times_per_feature.items():
if feature not in self._print_times_per_feature[build_plate_number]:
self._print_times_per_feature[build_plate_number][feature] = Duration(parent=self)
duration = self._print_times_per_feature[build_plate_number][feature]
for feature, time in print_time.items():
if time != time: # Check for NaN. Engine can sometimes give us weird values.
self._print_time_message_values[build_plate_number].get(feature).setDuration(0)
duration.setDuration(0)
Logger.log("w", "Received NaN for print duration message")
continue
total_estimated_time += time
self._print_time_message_values[build_plate_number].get(feature).setDuration(time)
duration.setDuration(time)
if build_plate_number not in self._current_print_time:
self._current_print_time[build_plate_number] = Duration(None, self)
self._current_print_time[build_plate_number].setDuration(total_estimated_time)
def _calculateInformation(self, build_plate_number):
def _calculateInformation(self, build_plate_number: int) -> None:
global_stack = self._application.getGlobalContainerStack()
if global_stack is None:
return
@ -213,39 +203,45 @@ class PrintInformation(QObject):
material_preference_values = json.loads(self._application.getInstance().getPreferences().getValue("cura/material_settings"))
extruder_stacks = global_stack.extruders
for position, extruder_stack in extruder_stacks.items():
for position in extruder_stacks:
extruder_stack = extruder_stacks[position]
index = int(position)
if index >= len(self._material_amounts):
continue
amount = self._material_amounts[index]
## Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
# list comprehension filtering to solve this for us.
# Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
# list comprehension filtering to solve this for us.
density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0)
material = extruder_stack.findContainer({"type": "material"})
material = extruder_stack.material
radius = extruder_stack.getProperty("material_diameter", "value") / 2
weight = float(amount) * float(density) / 1000
cost = 0
material_name = catalog.i18nc("@label unknown material", "Unknown")
if material:
material_guid = material.getMetaDataEntry("GUID")
material_name = material.getName()
if material_guid in material_preference_values:
material_values = material_preference_values[material_guid]
cost = 0.
weight_per_spool = float(material_values["spool_weight"] if material_values and "spool_weight" in material_values else 0)
cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0)
material_guid = material.getMetaDataEntry("GUID")
material_name = material.getName()
if material_guid in material_preference_values:
material_values = material_preference_values[material_guid]
if weight_per_spool != 0:
cost = cost_per_spool * weight / weight_per_spool
else:
cost = 0
if material_values and "spool_weight" in material_values:
weight_per_spool = float(material_values["spool_weight"])
else:
weight_per_spool = float(extruder_stack.getMetaDataEntry("properties", {}).get("weight", 0))
cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0)
if weight_per_spool != 0:
cost = cost_per_spool * weight / weight_per_spool
else:
cost = 0
# Material amount is sent as an amount of mm^3, so calculate length from that
if radius != 0:
length = round((amount / (math.pi * radius ** 2)) / 1000, 2)
else:
length = 0
self._material_weights[build_plate_number].append(weight)
self._material_lengths[build_plate_number].append(length)
self._material_costs[build_plate_number].append(cost)
@ -256,20 +252,20 @@ class PrintInformation(QObject):
self.materialCostsChanged.emit()
self.materialNamesChanged.emit()
def _onPreferencesChanged(self, preference):
def _onPreferencesChanged(self, preference: str) -> None:
if preference != "cura/material_settings":
return
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
self._calculateInformation(build_plate_number)
def _onActiveBuildPlateChanged(self):
def _onActiveBuildPlateChanged(self) -> None:
new_active_build_plate = self._multi_build_plate_model.activeBuildPlate
if new_active_build_plate != self._active_build_plate:
self._active_build_plate = new_active_build_plate
self._updateJobName()
self._initVariablesWithBuildPlate(self._active_build_plate)
self._initVariablesByBuildPlate(self._active_build_plate)
self.materialLengthsChanged.emit()
self.materialWeightsChanged.emit()
@ -277,14 +273,14 @@ class PrintInformation(QObject):
self.materialNamesChanged.emit()
self.currentPrintTimeChanged.emit()
def _onActiveMaterialsChanged(self, *args, **kwargs):
def _onActiveMaterialsChanged(self, *args, **kwargs) -> None:
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
self._calculateInformation(build_plate_number)
# Manual override of job name should also set the base name so that when the printer prefix is updated, it the
# prefix can be added to the manually added name, not the old base name
@pyqtSlot(str, bool)
def setJobName(self, name, is_user_specified_job_name = False):
def setJobName(self, name: str, is_user_specified_job_name = False) -> None:
self._is_user_specified_job_name = is_user_specified_job_name
self._job_name = name
self._base_name = name.replace(self._abbr_machine + "_", "")
@ -298,15 +294,15 @@ class PrintInformation(QObject):
def jobName(self):
return self._job_name
def _updateJobName(self):
def _updateJobName(self) -> None:
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:
@ -333,12 +329,12 @@ class PrintInformation(QObject):
self.jobNameChanged.emit()
@pyqtSlot(str)
def setProjectName(self, name):
def setProjectName(self, name: str) -> None:
self.setBaseName(name, is_project_file = True)
baseNameChanged = pyqtSignal()
def setBaseName(self, base_name: str, is_project_file: bool = False):
def setBaseName(self, base_name: str, is_project_file: bool = False) -> None:
self._is_user_specified_job_name = False
# Ensure that we don't use entire path but only filename
@ -373,6 +369,16 @@ class PrintInformation(QObject):
else:
self._base_name = ""
# Strip the old "curaproject" extension from the name
OLD_CURA_PROJECT_EXT = ".curaproject"
if self._base_name.lower().endswith(OLD_CURA_PROJECT_EXT):
self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_EXT)]
# CURA-5896 Try to strip extra extensions with an infinite amount of ".curaproject.3mf".
OLD_CURA_PROJECT_3MF_EXT = ".curaproject.3mf"
while self._base_name.lower().endswith(OLD_CURA_PROJECT_3MF_EXT):
self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_3MF_EXT)]
self._updateJobName()
@pyqtProperty(str, fset = setBaseName, notify = baseNameChanged)
@ -382,7 +388,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) -> None:
global_container_stack = self._application.getGlobalContainerStack()
if not global_container_stack:
self._abbr_machine = ""
@ -406,15 +412,15 @@ class PrintInformation(QObject):
self._abbr_machine = abbr_machine
## Utility method that strips accents from characters (eg: â -> a)
def _stripAccents(self, str):
return ''.join(char for char in unicodedata.normalize('NFD', str) if unicodedata.category(char) != 'Mn')
def _stripAccents(self, to_strip: str) -> str:
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
@pyqtSlot(result = "QVariantMap")
def getFeaturePrintTimes(self):
result = {}
if self._active_build_plate not in self._print_time_message_values:
self._initPrintTimeMessageValues(self._active_build_plate)
for feature, time in self._print_time_message_values[self._active_build_plate].items():
if self._active_build_plate not in self._print_times_per_feature:
self._initPrintTimesPerFeature(self._active_build_plate)
for feature, time in self._print_times_per_feature[self._active_build_plate].items():
if feature in self._print_time_message_translations:
result[self._print_time_message_translations[feature]] = time
else:
@ -422,22 +428,22 @@ class PrintInformation(QObject):
return result
# Simulate message with zero time duration
def setToZeroPrintInformation(self, build_plate = None):
def setToZeroPrintInformation(self, build_plate: Optional[int] = None) -> None:
if build_plate is None:
build_plate = self._active_build_plate
# Construct the 0-time message
temp_message = {}
if build_plate not in self._print_time_message_values:
self._print_time_message_values[build_plate] = {}
for key in self._print_time_message_values[build_plate].keys():
if build_plate not in self._print_times_per_feature:
self._print_times_per_feature[build_plate] = {}
for key in self._print_times_per_feature[build_plate].keys():
temp_message[key] = 0
temp_material_amounts = [0]
temp_material_amounts = [0.]
self._onPrintDurationMessage(build_plate, temp_message, temp_material_amounts)
## Listen to scene changes to check if we need to reset the print information
def _onSceneChanged(self, scene_node):
def _onSceneChanged(self, scene_node: SceneNode) -> None:
# Ignore any changes that are not related to sliceable objects
if not isinstance(scene_node, SceneNode)\
or not scene_node.callDecoration("isSliceable")\

View file

@ -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:

View file

@ -0,0 +1,78 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty
from enum import IntEnum
from threading import Thread
from typing import Union
MYPY = False
if MYPY:
from cura.PrinterOutputDevice import PrinterOutputDevice
class FirmwareUpdater(QObject):
firmwareProgressChanged = pyqtSignal()
firmwareUpdateStateChanged = pyqtSignal()
def __init__(self, output_device: "PrinterOutputDevice") -> None:
super().__init__()
self._output_device = output_device
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
self._firmware_file = ""
self._firmware_progress = 0
self._firmware_update_state = FirmwareUpdateState.idle
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
# the file path could be url-encoded.
if firmware_file.startswith("file://"):
self._firmware_file = QUrl(firmware_file).toLocalFile()
else:
self._firmware_file = firmware_file
self._setFirmwareUpdateState(FirmwareUpdateState.updating)
self._update_firmware_thread.start()
def _updateFirmware(self) -> None:
raise NotImplementedError("_updateFirmware needs to be implemented")
## Cleanup after a succesful update
def _cleanupAfterUpdate(self) -> None:
# Clean up for next attempt.
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
self._firmware_file = ""
self._onFirmwareProgress(100)
self._setFirmwareUpdateState(FirmwareUpdateState.completed)
@pyqtProperty(int, notify = firmwareProgressChanged)
def firmwareProgress(self) -> int:
return self._firmware_progress
@pyqtProperty(int, notify=firmwareUpdateStateChanged)
def firmwareUpdateState(self) -> "FirmwareUpdateState":
return self._firmware_update_state
def _setFirmwareUpdateState(self, state: "FirmwareUpdateState") -> None:
if self._firmware_update_state != state:
self._firmware_update_state = state
self.firmwareUpdateStateChanged.emit()
# Callback function for firmware update progress.
def _onFirmwareProgress(self, progress: int, max_progress: int = 100) -> None:
self._firmware_progress = int(progress * 100 / max_progress) # Convert to scale of 0-100
self.firmwareProgressChanged.emit()
class FirmwareUpdateState(IntEnum):
idle = 0
updating = 1
completed = 2
unknown_error = 3
communication_error = 4
io_error = 5
firmware_not_found_error = 6

View file

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Set, Union, Optional
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from PyQt5.QtCore import QTimer
@ -9,27 +9,28 @@ from PyQt5.QtCore import QTimer
if TYPE_CHECKING:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
class GenericOutputController(PrinterOutputController):
def __init__(self, output_device):
def __init__(self, output_device: "PrinterOutputDevice") -> None:
super().__init__(output_device)
self._preheat_bed_timer = QTimer()
self._preheat_bed_timer.setSingleShot(True)
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
self._preheat_printer = None
self._preheat_printer = None # type: Optional[PrinterOutputModel]
self._preheat_hotends_timer = QTimer()
self._preheat_hotends_timer.setSingleShot(True)
self._preheat_hotends_timer.timeout.connect(self._onPreheatHotendsTimerFinished)
self._preheat_hotends = set()
self._preheat_hotends = set() # type: Set[ExtruderOutputModel]
self._output_device.printersChanged.connect(self._onPrintersChanged)
self._active_printer = None
self._active_printer = None # type: Optional[PrinterOutputModel]
def _onPrintersChanged(self):
def _onPrintersChanged(self) -> None:
if self._active_printer:
self._active_printer.stateChanged.disconnect(self._onPrinterStateChanged)
self._active_printer.targetBedTemperatureChanged.disconnect(self._onTargetBedTemperatureChanged)
@ -43,32 +44,33 @@ class GenericOutputController(PrinterOutputController):
for extruder in self._active_printer.extruders:
extruder.targetHotendTemperatureChanged.connect(self._onTargetHotendTemperatureChanged)
def _onPrinterStateChanged(self):
if self._active_printer.state != "idle":
def _onPrinterStateChanged(self) -> None:
if self._active_printer and self._active_printer.state != "idle":
if self._preheat_bed_timer.isActive():
self._preheat_bed_timer.stop()
self._preheat_printer.updateIsPreheating(False)
if self._preheat_printer:
self._preheat_printer.updateIsPreheating(False)
if self._preheat_hotends_timer.isActive():
self._preheat_hotends_timer.stop()
for extruder in self._preheat_hotends:
extruder.updateIsPreheating(False)
self._preheat_hotends = set()
self._preheat_hotends = set() # type: Set[ExtruderOutputModel]
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
self._output_device.sendCommand("G91")
self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
self._output_device.sendCommand("G90")
def homeHead(self, printer):
def homeHead(self, printer: "PrinterOutputModel") -> None:
self._output_device.sendCommand("G28 X Y")
def homeBed(self, printer):
def homeBed(self, printer: "PrinterOutputModel") -> None:
self._output_device.sendCommand("G28 Z")
def sendRawCommand(self, printer: "PrinterOutputModel", command: str):
self._output_device.sendCommand(command)
def sendRawCommand(self, printer: "PrinterOutputModel", command: str) -> None:
self._output_device.sendCommand(command.upper()) #Most printers only understand uppercase g-code commands.
def setJobState(self, job: "PrintJobOutputModel", state: str):
def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:
if state == "pause":
self._output_device.pausePrint()
job.updateState("paused")
@ -79,15 +81,15 @@ class GenericOutputController(PrinterOutputController):
self._output_device.cancelPrint()
pass
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None:
self._output_device.sendCommand("M140 S%s" % temperature)
def _onTargetBedTemperatureChanged(self):
if self._preheat_bed_timer.isActive() and self._preheat_printer.targetBedTemperature == 0:
def _onTargetBedTemperatureChanged(self) -> None:
if self._preheat_bed_timer.isActive() and self._preheat_printer and self._preheat_printer.targetBedTemperature == 0:
self._preheat_bed_timer.stop()
self._preheat_printer.updateIsPreheating(False)
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration) -> None:
try:
temperature = round(temperature) # The API doesn't allow floating point.
duration = round(duration)
@ -100,21 +102,25 @@ class GenericOutputController(PrinterOutputController):
self._preheat_printer = printer
printer.updateIsPreheating(True)
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
self.setTargetBedTemperature(printer, temperature=0)
self._preheat_bed_timer.stop()
printer.updateIsPreheating(False)
def _onPreheatBedTimerFinished(self):
def _onPreheatBedTimerFinished(self) -> None:
if not self._preheat_printer:
return
self.setTargetBedTemperature(self._preheat_printer, 0)
self._preheat_printer.updateIsPreheating(False)
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: int):
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: Union[int, float]) -> None:
self._output_device.sendCommand("M104 S%s T%s" % (temperature, position))
def _onTargetHotendTemperatureChanged(self):
def _onTargetHotendTemperatureChanged(self) -> None:
if not self._preheat_hotends_timer.isActive():
return
if not self._active_printer:
return
for extruder in self._active_printer.extruders:
if extruder in self._preheat_hotends and extruder.targetHotendTemperature == 0:
@ -123,7 +129,7 @@ class GenericOutputController(PrinterOutputController):
if not self._preheat_hotends:
self._preheat_hotends_timer.stop()
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration):
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration) -> None:
position = extruder.getPosition()
number_of_extruders = len(extruder.getPrinter().extruders)
if position >= number_of_extruders:
@ -141,7 +147,7 @@ class GenericOutputController(PrinterOutputController):
self._preheat_hotends.add(extruder)
extruder.updateIsPreheating(True)
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"):
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel") -> None:
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), temperature=0)
if extruder in self._preheat_hotends:
extruder.updateIsPreheating(False)
@ -149,21 +155,22 @@ class GenericOutputController(PrinterOutputController):
if not self._preheat_hotends and self._preheat_hotends_timer.isActive():
self._preheat_hotends_timer.stop()
def _onPreheatHotendsTimerFinished(self):
def _onPreheatHotendsTimerFinished(self) -> None:
for extruder in self._preheat_hotends:
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0)
self._preheat_hotends = set()
self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
# Cancel any ongoing preheating timers, without setting back the temperature to 0
# This can be used eg at the start of a print
def stopPreheatTimers(self):
def stopPreheatTimers(self) -> None:
if self._preheat_hotends_timer.isActive():
for extruder in self._preheat_hotends:
extruder.updateIsPreheating(False)
self._preheat_hotends = set()
self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
self._preheat_hotends_timer.stop()
if self._preheat_bed_timer.isActive():
self._preheat_printer.updateIsPreheating(False)
if self._preheat_printer:
self._preheat_printer.updateIsPreheating(False)
self._preheat_bed_timer.stop()

View file

@ -1,119 +0,0 @@
from UM.Logger import Logger
from PyQt5.QtCore import QUrl, pyqtProperty, pyqtSignal, QObject, pyqtSlot
from PyQt5.QtGui import QImage
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
class NetworkCamera(QObject):
newImage = pyqtSignal()
def __init__(self, target = None, parent = None):
super().__init__(parent)
self._stream_buffer = b""
self._stream_buffer_start_index = -1
self._manager = None
self._image_request = None
self._image_reply = None
self._image = QImage()
self._image_id = 0
self._target = target
self._started = False
@pyqtSlot(str)
def setTarget(self, target):
restart_required = False
if self._started:
self.stop()
restart_required = True
self._target = target
if restart_required:
self.start()
@pyqtProperty(QUrl, notify=newImage)
def latestImage(self):
self._image_id += 1
# There is an image provider that is called "camera". In order to ensure that the image qml object, that
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
# as new (instead of relying on cached version and thus forces an update.
temp = "image://camera/" + str(self._image_id)
return QUrl(temp, QUrl.TolerantMode)
@pyqtSlot()
def start(self):
# Ensure that previous requests (if any) are stopped.
self.stop()
if self._target is None:
Logger.log("w", "Unable to start camera stream without target!")
return
self._started = True
url = QUrl(self._target)
self._image_request = QNetworkRequest(url)
if self._manager is None:
self._manager = QNetworkAccessManager()
self._image_reply = self._manager.get(self._image_request)
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
@pyqtSlot()
def stop(self):
self._stream_buffer = b""
self._stream_buffer_start_index = -1
if self._image_reply:
try:
# disconnect the signal
try:
self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
except Exception:
pass
# abort the request if it's not finished
if not self._image_reply.isFinished():
self._image_reply.close()
except Exception as e: # RuntimeError
pass # It can happen that the wrapped c++ object is already deleted.
self._image_reply = None
self._image_request = None
self._manager = None
self._started = False
def getImage(self):
return self._image
## Ensure that close gets called when object is destroyed
def __del__(self):
self.stop()
def _onStreamDownloadProgress(self, bytes_received, bytes_total):
# An MJPG stream is (for our purpose) a stream of concatenated JPG images.
# JPG images start with the marker 0xFFD8, and end with 0xFFD9
if self._image_reply is None:
return
self._stream_buffer += self._image_reply.readAll()
if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger
Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
self.stop() # resets stream buffer and start index
self.start()
return
if self._stream_buffer_start_index == -1:
self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
# If this happens to be more than a single frame, then so be it; the JPG decoder will
# ignore the extra data. We do it like this in order not to get a buildup of frames
if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
self._stream_buffer_start_index = -1
self._image.loadFromData(jpg_data)
self.newImage.emit()

View file

@ -0,0 +1,153 @@
# Copyright (c) 2018 Aldo Hoeben / fieldOfView
# NetworkMJPGImage is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QUrl, pyqtProperty, pyqtSignal, pyqtSlot, QRect, QByteArray
from PyQt5.QtGui import QImage, QPainter
from PyQt5.QtQuick import QQuickPaintedItem
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
from UM.Logger import Logger
#
# A QQuickPaintedItem that progressively downloads a network mjpeg stream,
# picks it apart in individual jpeg frames, and paints it.
#
class NetworkMJPGImage(QQuickPaintedItem):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self._stream_buffer = QByteArray()
self._stream_buffer_start_index = -1
self._network_manager = None # type: QNetworkAccessManager
self._image_request = None # type: QNetworkRequest
self._image_reply = None # type: QNetworkReply
self._image = QImage()
self._image_rect = QRect()
self._source_url = QUrl()
self._started = False
self._mirror = False
self.setAntialiasing(True)
## Ensure that close gets called when object is destroyed
def __del__(self) -> None:
self.stop()
def paint(self, painter: "QPainter") -> None:
if self._mirror:
painter.drawImage(self.contentsBoundingRect(), self._image.mirrored())
return
painter.drawImage(self.contentsBoundingRect(), self._image)
def setSourceURL(self, source_url: "QUrl") -> None:
self._source_url = source_url
self.sourceURLChanged.emit()
if self._started:
self.start()
def getSourceURL(self) -> "QUrl":
return self._source_url
sourceURLChanged = pyqtSignal()
source = pyqtProperty(QUrl, fget = getSourceURL, fset = setSourceURL, notify = sourceURLChanged)
def setMirror(self, mirror: bool) -> None:
if mirror == self._mirror:
return
self._mirror = mirror
self.mirrorChanged.emit()
self.update()
def getMirror(self) -> bool:
return self._mirror
mirrorChanged = pyqtSignal()
mirror = pyqtProperty(bool, fget = getMirror, fset = setMirror, notify = mirrorChanged)
imageSizeChanged = pyqtSignal()
@pyqtProperty(int, notify = imageSizeChanged)
def imageWidth(self) -> int:
return self._image.width()
@pyqtProperty(int, notify = imageSizeChanged)
def imageHeight(self) -> int:
return self._image.height()
@pyqtSlot()
def start(self) -> None:
self.stop() # Ensure that previous requests (if any) are stopped.
if not self._source_url:
Logger.log("w", "Unable to start camera stream without target!")
return
self._started = True
self._image_request = QNetworkRequest(self._source_url)
if self._network_manager is None:
self._network_manager = QNetworkAccessManager()
self._image_reply = self._network_manager.get(self._image_request)
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
@pyqtSlot()
def stop(self) -> None:
self._stream_buffer = QByteArray()
self._stream_buffer_start_index = -1
if self._image_reply:
try:
try:
self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
except Exception:
pass
if not self._image_reply.isFinished():
self._image_reply.close()
except Exception as e: # RuntimeError
pass # It can happen that the wrapped c++ object is already deleted.
self._image_reply = None
self._image_request = None
self._network_manager = None
self._started = False
def _onStreamDownloadProgress(self, bytes_received: int, bytes_total: int) -> None:
# An MJPG stream is (for our purpose) a stream of concatenated JPG images.
# JPG images start with the marker 0xFFD8, and end with 0xFFD9
if self._image_reply is None:
return
self._stream_buffer += self._image_reply.readAll()
if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger
Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
self.stop() # resets stream buffer and start index
self.start()
return
if self._stream_buffer_start_index == -1:
self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
# If this happens to be more than a single frame, then so be it; the JPG decoder will
# ignore the extra data. We do it like this in order not to get a buildup of frames
if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
self._stream_buffer_start_index = -1
self._image.loadFromData(jpg_data)
if self._image.rect() != self._image_rect:
self.imageSizeChanged.emit()
self.update()

View file

@ -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")
@ -143,9 +130,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
# We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
# sleep.
if time_since_last_response > self._recreate_network_manager_time:
if self._last_manager_create_time is None:
self._createNetworkManager()
elif time() - self._last_manager_create_time > self._recreate_network_manager_time:
if self._last_manager_create_time is None or time() - self._last_manager_create_time > self._recreate_network_manager_time:
self._createNetworkManager()
assert(self._manager is not None)
elif self._connection_state == ConnectionState.closed:
@ -228,7 +213,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
request = self._createEmptyRequest(target)
self._last_request_time = time()
if self._manager is not None:
reply = self._manager.post(request, data)
reply = self._manager.post(request, data.encode())
if on_progress is not None:
reply.uploadProgress.connect(on_progress)
self._registerOnFinishedCallback(reply, on_finished)
@ -341,7 +326,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)

View file

@ -1,4 +1,4 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
@ -12,7 +12,6 @@ if TYPE_CHECKING:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
class PrintJobOutputModel(QObject):
stateChanged = pyqtSignal()
timeTotalChanged = pyqtSignal()
@ -55,7 +54,7 @@ class PrintJobOutputModel(QObject):
@pyqtProperty(QUrl, notify=previewImageChanged)
def previewImageUrl(self):
self._preview_image_id += 1
# There is an image provider that is called "camera". In order to ensure that the image qml object, that
# There is an image provider that is called "print_job_preview". In order to ensure that the image qml object, that
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
# as new (instead of relying on cached version and thus forces an update.
temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
@ -91,7 +90,7 @@ class PrintJobOutputModel(QObject):
def assignedPrinter(self):
return self._assigned_printer
def updateAssignedPrinter(self, assigned_printer: "PrinterOutputModel"):
def updateAssignedPrinter(self, assigned_printer: Optional["PrinterOutputModel"]) -> None:
if self._assigned_printer != assigned_printer:
old_printer = self._assigned_printer
self._assigned_printer = assigned_printer
@ -147,4 +146,4 @@ class PrintJobOutputModel(QObject):
@pyqtSlot(str)
def setState(self, state):
self._output_controller.setJobState(self, state)
self._output_controller.setJobState(self, state)

View file

@ -1,57 +1,68 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from UM.Signal import Signal
from typing import Union
MYPY = False
if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
class PrinterOutputController:
def __init__(self, output_device):
def __init__(self, output_device: "PrinterOutputDevice") -> None:
self.can_pause = True
self.can_abort = True
self.can_pre_heat_bed = True
self.can_pre_heat_hotends = True
self.can_send_raw_gcode = True
self.can_control_manually = True
self.can_update_firmware = False
self._output_device = output_device
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOutputModel", temperature: int):
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: Union[int, float]) -> None:
Logger.log("w", "Set target hotend temperature not implemented in controller")
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None:
Logger.log("w", "Set target bed temperature not implemented in controller")
def setJobState(self, job: "PrintJobOutputModel", state: str):
def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:
Logger.log("w", "Set job state not implemented in controller")
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
Logger.log("w", "Cancel preheat bed not implemented in controller")
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration) -> None:
Logger.log("w", "Preheat bed not implemented in controller")
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"):
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel") -> None:
Logger.log("w", "Cancel preheat hotend not implemented in controller")
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration):
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration) -> None:
Logger.log("w", "Preheat hotend not implemented in controller")
def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed):
def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
Logger.log("w", "Set head position not implemented in controller")
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
Logger.log("w", "Move head not implemented in controller")
def homeBed(self, printer: "PrinterOutputModel"):
def homeBed(self, printer: "PrinterOutputModel") -> None:
Logger.log("w", "Home bed not implemented in controller")
def homeHead(self, printer: "PrinterOutputModel"):
def homeHead(self, printer: "PrinterOutputModel") -> None:
Logger.log("w", "Home head not implemented in controller")
def sendRawCommand(self, printer: "PrinterOutputModel", command: str):
def sendRawCommand(self, printer: "PrinterOutputModel", command: str) -> None:
Logger.log("w", "Custom command not implemented in controller")
canUpdateFirmwareChanged = Signal()
def setCanUpdateFirmware(self, can_update_firmware: bool) -> None:
if can_update_firmware != self.can_update_firmware:
self.can_update_firmware = can_update_firmware
self.canUpdateFirmwareChanged.emit()

View file

@ -1,8 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
from typing import Optional
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl
from typing import List, Dict, Optional
from UM.Math.Vector import Vector
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
@ -24,8 +24,9 @@ class PrinterOutputModel(QObject):
keyChanged = pyqtSignal()
printerTypeChanged = pyqtSignal()
buildplateChanged = pyqtSignal()
cameraChanged = pyqtSignal()
cameraUrlChanged = pyqtSignal()
configurationChanged = pyqtSignal()
canUpdateFirmwareChanged = pyqtSignal()
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
super().__init__(parent)
@ -34,6 +35,7 @@ class PrinterOutputModel(QObject):
self._name = ""
self._key = "" # Unique identifier
self._controller = output_controller
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
self._head_position = Vector(0, 0, 0)
@ -42,40 +44,40 @@ class PrinterOutputModel(QObject):
self._printer_state = "unknown"
self._is_preheating = False
self._printer_type = ""
self._buildplate_name = None
self._buildplate_name = ""
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
self._extruders]
self._camera = None
self._camera_url = QUrl() # type: QUrl
@pyqtProperty(str, constant = True)
def firmwareVersion(self):
def firmwareVersion(self) -> str:
return self._firmware_version
def setCamera(self, camera):
if self._camera is not camera:
self._camera = camera
self.cameraChanged.emit()
def setCameraUrl(self, camera_url: "QUrl") -> None:
if self._camera_url != camera_url:
self._camera_url = camera_url
self.cameraUrlChanged.emit()
def updateIsPreheating(self, pre_heating):
@pyqtProperty(QUrl, fset = setCameraUrl, notify = cameraUrlChanged)
def cameraUrl(self) -> "QUrl":
return self._camera_url
def updateIsPreheating(self, pre_heating: bool) -> None:
if self._is_preheating != pre_heating:
self._is_preheating = pre_heating
self.isPreheatingChanged.emit()
@pyqtProperty(bool, notify=isPreheatingChanged)
def isPreheating(self):
def isPreheating(self) -> bool:
return self._is_preheating
@pyqtProperty(QObject, notify=cameraChanged)
def camera(self):
return self._camera
@pyqtProperty(str, notify = printerTypeChanged)
def type(self):
def type(self) -> str:
return self._printer_type
def updateType(self, printer_type):
def updateType(self, printer_type: str) -> None:
if self._printer_type != printer_type:
self._printer_type = printer_type
self._printer_configuration.printerType = self._printer_type
@ -83,10 +85,10 @@ class PrinterOutputModel(QObject):
self.configurationChanged.emit()
@pyqtProperty(str, notify = buildplateChanged)
def buildplate(self):
def buildplate(self) -> str:
return self._buildplate_name
def updateBuildplateName(self, buildplate_name):
def updateBuildplateName(self, buildplate_name: str) -> None:
if self._buildplate_name != buildplate_name:
self._buildplate_name = buildplate_name
self._printer_configuration.buildplateConfiguration = self._buildplate_name
@ -94,66 +96,66 @@ class PrinterOutputModel(QObject):
self.configurationChanged.emit()
@pyqtProperty(str, notify=keyChanged)
def key(self):
def key(self) -> str:
return self._key
def updateKey(self, key: str):
def updateKey(self, key: str) -> None:
if self._key != key:
self._key = key
self.keyChanged.emit()
@pyqtSlot()
def homeHead(self):
def homeHead(self) -> None:
self._controller.homeHead(self)
@pyqtSlot()
def homeBed(self):
def homeBed(self) -> None:
self._controller.homeBed(self)
@pyqtSlot(str)
def sendRawCommand(self, command: str):
def sendRawCommand(self, command: str) -> None:
self._controller.sendRawCommand(self, command)
@pyqtProperty("QVariantList", constant = True)
def extruders(self):
def extruders(self) -> List["ExtruderOutputModel"]:
return self._extruders
@pyqtProperty(QVariant, notify = headPositionChanged)
def headPosition(self):
def headPosition(self) -> Dict[str, float]:
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z}
def updateHeadPosition(self, x, y, z):
def updateHeadPosition(self, x: float, y: float, z: float) -> None:
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
self._head_position = Vector(x, y, z)
self.headPositionChanged.emit()
@pyqtProperty(float, float, float)
@pyqtProperty(float, float, float, float)
def setHeadPosition(self, x, y, z, speed = 3000):
def setHeadPosition(self, x: float, y: float, z: float, speed: float = 3000) -> None:
self.updateHeadPosition(x, y, z)
self._controller.setHeadPosition(self, x, y, z, speed)
@pyqtProperty(float)
@pyqtProperty(float, float)
def setHeadX(self, x, speed = 3000):
def setHeadX(self, x: float, speed: float = 3000) -> None:
self.updateHeadPosition(x, self._head_position.y, self._head_position.z)
self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
@pyqtProperty(float)
@pyqtProperty(float, float)
def setHeadY(self, y, speed = 3000):
def setHeadY(self, y: float, speed: float = 3000) -> None:
self.updateHeadPosition(self._head_position.x, y, self._head_position.z)
self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
@pyqtProperty(float)
@pyqtProperty(float, float)
def setHeadZ(self, z, speed = 3000):
def setHeadZ(self, z: float, speed:float = 3000) -> None:
self.updateHeadPosition(self._head_position.x, self._head_position.y, z)
self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
@pyqtSlot(float, float, float)
@pyqtSlot(float, float, float, float)
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000):
def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None:
self._controller.moveHead(self, x, y, z, speed)
## Pre-heats the heated bed of the printer.
@ -162,47 +164,47 @@ class PrinterOutputModel(QObject):
# Celsius.
# \param duration How long the bed should stay warm, in seconds.
@pyqtSlot(float, float)
def preheatBed(self, temperature, duration):
def preheatBed(self, temperature: float, duration: float) -> None:
self._controller.preheatBed(self, temperature, duration)
@pyqtSlot()
def cancelPreheatBed(self):
def cancelPreheatBed(self) -> None:
self._controller.cancelPreheatBed(self)
def getController(self):
def getController(self) -> "PrinterOutputController":
return self._controller
@pyqtProperty(str, notify=nameChanged)
def name(self):
@pyqtProperty(str, notify = nameChanged)
def name(self) -> str:
return self._name
def setName(self, name):
def setName(self, name: str) -> None:
self._setName(name)
self.updateName(name)
def updateName(self, name):
def updateName(self, name: str) -> None:
if self._name != name:
self._name = name
self.nameChanged.emit()
## Update the bed temperature. This only changes it locally.
def updateBedTemperature(self, temperature):
def updateBedTemperature(self, temperature: int) -> None:
if self._bed_temperature != temperature:
self._bed_temperature = temperature
self.bedTemperatureChanged.emit()
def updateTargetBedTemperature(self, temperature):
def updateTargetBedTemperature(self, temperature: int) -> None:
if self._target_bed_temperature != temperature:
self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit()
## Set the target bed temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(int)
def setTargetBedTemperature(self, temperature):
def setTargetBedTemperature(self, temperature: int) -> None:
self._controller.setTargetBedTemperature(self, temperature)
self.updateTargetBedTemperature(temperature)
def updateActivePrintJob(self, print_job):
def updateActivePrintJob(self, print_job: Optional["PrintJobOutputModel"]) -> None:
if self._active_print_job != print_job:
old_print_job = self._active_print_job
@ -214,72 +216,83 @@ class PrinterOutputModel(QObject):
old_print_job.updateAssignedPrinter(None)
self.activePrintJobChanged.emit()
def updateState(self, printer_state):
def updateState(self, printer_state: str) -> None:
if self._printer_state != printer_state:
self._printer_state = printer_state
self.stateChanged.emit()
@pyqtProperty(QObject, notify = activePrintJobChanged)
def activePrintJob(self):
def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
return self._active_print_job
@pyqtProperty(str, notify=stateChanged)
def state(self):
def state(self) -> str:
return self._printer_state
@pyqtProperty(int, notify = bedTemperatureChanged)
def bedTemperature(self):
@pyqtProperty(int, notify=bedTemperatureChanged)
def bedTemperature(self) -> int:
return self._bed_temperature
@pyqtProperty(int, notify=targetBedTemperatureChanged)
def targetBedTemperature(self):
def targetBedTemperature(self) -> int:
return self._target_bed_temperature
# Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True)
def canPreHeatBed(self):
def canPreHeatBed(self) -> bool:
if self._controller:
return self._controller.can_pre_heat_bed
return False
# Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True)
def canPreHeatHotends(self):
def canPreHeatHotends(self) -> bool:
if self._controller:
return self._controller.can_pre_heat_hotends
return False
# Does the printer support sending raw G-code at all
@pyqtProperty(bool, constant=True)
def canSendRawGcode(self):
def canSendRawGcode(self) -> bool:
if self._controller:
return self._controller.can_send_raw_gcode
return False
# Does the printer support pause at all
@pyqtProperty(bool, constant=True)
def canPause(self):
def canPause(self) -> bool:
if self._controller:
return self._controller.can_pause
return False
# Does the printer support abort at all
@pyqtProperty(bool, constant=True)
def canAbort(self):
def canAbort(self) -> bool:
if self._controller:
return self._controller.can_abort
return False
# Does the printer support manual control at all
@pyqtProperty(bool, constant=True)
def canControlManually(self):
def canControlManually(self) -> bool:
if self._controller:
return self._controller.can_control_manually
return False
# Does the printer support upgrading firmware
@pyqtProperty(bool, notify = canUpdateFirmwareChanged)
def canUpdateFirmware(self) -> bool:
if self._controller:
return self._controller.can_update_firmware
return False
# Stub to connect UM.Signal to pyqtSignal
def _onControllerCanUpdateFirmwareChanged(self) -> None:
self.canUpdateFirmwareChanged.emit()
# Returns the configuration (material, variant and buildplate) of the current printer
@pyqtProperty(QObject, notify = configurationChanged)
def printerConfiguration(self):
def printerConfiguration(self) -> Optional[ConfigurationModel]:
if self._printer_configuration.isValid():
return self._printer_configuration
return None

View file

@ -4,22 +4,24 @@
from UM.Decorators import deprecated
from UM.i18n import i18nCatalog
from UM.OutputDevice.OutputDevice import OutputDevice
from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
from PyQt5.QtWidgets import QMessageBox
from UM.Logger import Logger
from UM.FileHandler.FileHandler import FileHandler #For typing.
from UM.Scene.SceneNode import SceneNode #For typing.
from UM.Signal import signalemitter
from UM.Qt.QtApplication import QtApplication
from UM.FlameProfiler import pyqtSlot
from enum import IntEnum # For the connection state tracking.
from typing import Callable, List, Optional
from typing import Callable, List, Optional, Union
MYPY = False
if MYPY:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdater
from UM.FileHandler.FileHandler import FileHandler
from UM.Scene.SceneNode import SceneNode
i18n_catalog = i18nCatalog("cura")
@ -83,6 +85,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
self._connection_state = ConnectionState.closed #type: ConnectionState
self._firmware_updater = None #type: Optional[FirmwareUpdater]
self._firmware_name = None #type: Optional[str]
self._address = "" #type: str
self._connection_text = "" #type: str
@ -128,7 +131,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
return None
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
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")
@pyqtProperty(QObject, notify = printersChanged)
@ -225,4 +228,14 @@ class PrinterOutputDevice(QObject, OutputDevice):
#
# This name can be used to define device type
def getFirmwareName(self) -> Optional[str]:
return self._firmware_name
return self._firmware_name
def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:
return self._firmware_updater
@pyqtSlot(str)
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
if not self._firmware_updater:
return
self._firmware_updater.updateFirmware(firmware_file)

View file

@ -1,9 +1,12 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
class BlockSlicingDecorator(SceneNodeDecorator):
def __init__(self):
def __init__(self) -> None:
super().__init__()
def isBlockSlicing(self):
def isBlockSlicing(self) -> bool:
return True

View file

@ -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,26 +121,33 @@ 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):
# If the tool operation is still active, we need to compute the convex hull later after the controller is
# no longer active.
if controller.isToolOperationActive():
self.recomputeConvexHullDelayed()
return
if self._convex_hull_node:
self._convex_hull_node.setParent(None)
self._convex_hull_node = None
@ -132,27 +159,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 +207,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 +255,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 +299,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 +310,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 +331,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 +357,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 +377,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:

View file

@ -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

View file

@ -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)()

View file

@ -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

View file

@ -4,12 +4,12 @@
import os
import urllib.parse
import uuid
from typing import Any
from typing import Dict, Union, Optional
from typing import Dict, Union, Any, TYPE_CHECKING, List
from PyQt5.QtCore import QObject, QUrl, QVariant
from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtWidgets import QMessageBox
from UM.i18n import i18nCatalog
from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger
@ -21,6 +21,18 @@ from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.MaterialNode import MaterialNode
from cura.Machines.QualityChangesGroup import QualityChangesGroup
from UM.PluginRegistry import PluginRegistry
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")
@ -31,20 +43,20 @@ catalog = i18nCatalog("cura")
# when a certain action happens. This can be done through this class.
class ContainerManager(QObject):
def __init__(self, application):
def __init__(self, application: "CuraApplication") -> None:
if ContainerManager.__instance is not None:
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
ContainerManager.__instance = self
super().__init__(parent = application)
self._application = application
self._plugin_registry = self._application.getPluginRegistry()
self._container_registry = self._application.getContainerRegistry()
self._machine_manager = self._application.getMachineManager()
self._material_manager = self._application.getMaterialManager()
self._quality_manager = self._application.getQualityManager()
self._container_name_filters = {} # type: Dict[str, Dict[str, Any]]
self._application = application # type: CuraApplication
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
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
self._container_name_filters = {} # type: Dict[str, Dict[str, Any]]
@pyqtSlot(str, str, result=str)
def getContainerMetaDataEntry(self, container_id: str, entry_names: str) -> str:
@ -69,21 +81,23 @@ class ContainerManager(QObject):
# by using "/" as a separator. For example, to change an entry "foo" in a
# dictionary entry "bar", you can specify "bar/foo" as entry name.
#
# \param container_id \type{str} The ID of the container to change.
# \param container_node \type{ContainerNode}
# \param entry_name \type{str} The name of the metadata entry to change.
# \param entry_value The new value of the entry.
#
# \return True if successful, False if not.
# TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this.
# Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want?
@pyqtSlot("QVariant", str, str)
def setContainerMetaDataEntry(self, container_node, entry_name, entry_value):
root_material_id = container_node.metadata["base_file"]
def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool:
root_material_id = container_node.getMetaDataEntry("base_file", "")
if self._container_registry.isReadOnly(root_material_id):
Logger.log("w", "Cannot set metadata of read-only container %s.", root_material_id)
return False
material_group = self._material_manager.getMaterialGroup(root_material_id)
if material_group is None:
Logger.log("w", "Unable to find material group for: %s.", root_material_id)
return False
entries = entry_name.split("/")
entry_name = entries.pop()
@ -91,11 +105,11 @@ class ContainerManager(QObject):
sub_item_changed = False
if entries:
root_name = entries.pop(0)
root = material_group.root_material_node.metadata.get(root_name)
root = material_group.root_material_node.getMetaDataEntry(root_name)
item = root
for _ in range(len(entries)):
item = item.get(entries.pop(0), { })
item = item.get(entries.pop(0), {})
if item[entry_name] != entry_value:
sub_item_changed = True
@ -109,9 +123,10 @@ class ContainerManager(QObject):
container.setMetaDataEntry(entry_name, entry_value)
if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed.
container.metaDataChanged.emit(container)
return True
@pyqtSlot(str, result = str)
def makeUniqueName(self, original_name):
def makeUniqueName(self, original_name: str) -> str:
return self._container_registry.uniqueName(original_name)
## Get a list of string that can be used as name filters for a Qt File Dialog
@ -125,7 +140,7 @@ class ContainerManager(QObject):
#
# \return A string list with name filters.
@pyqtSlot(str, result = "QStringList")
def getContainerNameFilters(self, type_name):
def getContainerNameFilters(self, type_name: str) -> List[str]:
if not self._container_name_filters:
self._updateContainerNameFilters()
@ -257,7 +272,7 @@ class ContainerManager(QObject):
#
# \return \type{bool} True if successful, False if not.
@pyqtSlot(result = bool)
def updateQualityChanges(self):
def updateQualityChanges(self) -> bool:
global_stack = self._machine_manager.activeMachine
if not global_stack:
return False
@ -313,10 +328,10 @@ class ContainerManager(QObject):
# \param material_id \type{str} the id of the material for which to get the linked materials.
# \return \type{list} a list of names of materials with the same GUID
@pyqtSlot("QVariant", bool, result = "QStringList")
def getLinkedMaterials(self, material_node, exclude_self = False):
guid = material_node.metadata["GUID"]
def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False):
guid = material_node.getMetaDataEntry("GUID", "")
self_root_material_id = material_node.metadata["base_file"]
self_root_material_id = material_node.getMetaDataEntry("base_file")
material_group_list = self._material_manager.getMaterialGroupListByGUID(guid)
linked_material_names = []
@ -324,15 +339,19 @@ class ContainerManager(QObject):
for material_group in material_group_list:
if exclude_self and material_group.name == self_root_material_id:
continue
linked_material_names.append(material_group.root_material_node.metadata["name"])
linked_material_names.append(material_group.root_material_node.getMetaDataEntry("name", ""))
return linked_material_names
## Unlink a material from all other materials by creating a new GUID
# \param material_id \type{str} the id of the material to create a new GUID for.
@pyqtSlot("QVariant")
def unlinkMaterial(self, material_node):
def unlinkMaterial(self, material_node: "MaterialNode") -> None:
# Get the material group
material_group = self._material_manager.getMaterialGroup(material_node.metadata["base_file"])
material_group = self._material_manager.getMaterialGroup(material_node.getMetaDataEntry("base_file", ""))
if material_group is None:
Logger.log("w", "Unable to find material group for %s", material_node)
return
# Generate a new GUID
new_guid = str(uuid.uuid4())
@ -344,7 +363,7 @@ class ContainerManager(QObject):
if container is not None:
container.setMetaDataEntry("GUID", new_guid)
def _performMerge(self, merge_into, merge, clear_settings = True):
def _performMerge(self, merge_into: InstanceContainer, merge: InstanceContainer, clear_settings: bool = True) -> None:
if merge == merge_into:
return
@ -372,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,
@ -400,7 +420,7 @@ class ContainerManager(QObject):
## Import single profile, file_url does not have to end with curaprofile
@pyqtSlot(QUrl, result="QVariantMap")
def importProfile(self, file_url):
def importProfile(self, file_url: QUrl):
if not file_url.isValid():
return
path = file_url.toLocalFile()
@ -409,7 +429,7 @@ class ContainerManager(QObject):
return self._container_registry.importProfile(path)
@pyqtSlot(QObject, QUrl, str)
def exportQualityChangesGroup(self, quality_changes_group, file_url: QUrl, file_type: str):
def exportQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", file_url: QUrl, file_type: str) -> None:
if not file_url.isValid():
return
path = file_url.toLocalFile()

View file

@ -187,11 +187,11 @@ class CuraContainerRegistry(ContainerRegistry):
try:
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
except NoProfileException:
return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "No custom profile to import in file <filename>{0}</filename>", file_name)}
return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "No custom profile to import in file <filename>{0}</filename>", file_name)}
except Exception as e:
# Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e))
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "\n" + str(e))}
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>:", file_name) + "\n<message>" + str(e) + "</message>"}
if profile_or_list:
# Ensure it is always a list of profiles
@ -215,7 +215,7 @@ class CuraContainerRegistry(ContainerRegistry):
if not global_profile:
Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name)
return { "status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)}
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)}
profile_definition = global_profile.getMetaDataEntry("definition")
# Make sure we have a profile_definition in the file:
@ -225,7 +225,7 @@ class CuraContainerRegistry(ContainerRegistry):
if not machine_definition:
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
return {"status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)
}
machine_definition = machine_definition[0]
@ -238,7 +238,7 @@ class CuraContainerRegistry(ContainerRegistry):
if profile_definition != expected_machine_definition:
Logger.log("e", "Profile [%s] is for machine [%s] but the current active machine is [%s]. Will not import the profile", file_name, profile_definition, expected_machine_definition)
return { "status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "The machine defined in profile <filename>{0}</filename> ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)}
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "The machine defined in profile <filename>{0}</filename> ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)}
# Fix the global quality profile's definition field in case it's not correct
global_profile.setMetaDataEntry("definition", expected_machine_definition)
@ -269,8 +269,7 @@ class CuraContainerRegistry(ContainerRegistry):
if idx == 0:
# move all per-extruder settings to the first extruder's quality_changes
for qc_setting_key in global_profile.getAllKeys():
settable_per_extruder = global_stack.getProperty(qc_setting_key,
"settable_per_extruder")
settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder:
setting_value = global_profile.getProperty(qc_setting_key, "value")
@ -310,8 +309,8 @@ class CuraContainerRegistry(ContainerRegistry):
if result is not None:
return {"status": "error", "message": catalog.i18nc(
"@info:status Don't translate the XML tags <filename> or <message>!",
"Failed to import profile from <filename>{0}</filename>: <message>{1}</message>",
file_name, result)}
"Failed to import profile from <filename>{0}</filename>:",
file_name) + " <message>" + result + "</message>"}
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
@ -686,7 +685,7 @@ class CuraContainerRegistry(ContainerRegistry):
if not os.path.isfile(file_path):
continue
parser = configparser.ConfigParser(interpolation=None)
parser = configparser.ConfigParser(interpolation = None)
try:
parser.read([file_path])
except:

View file

@ -145,13 +145,11 @@ class CuraContainerStack(ContainerStack):
def setDefinition(self, new_definition: DefinitionContainerInterface) -> None:
self.replaceContainer(_ContainerIndexes.Definition, new_definition)
## Get the definition container.
#
# \return The definition container. Should always be a valid container, but can be equal to the empty InstanceContainer.
@pyqtProperty(QObject, fset = setDefinition, notify = pyqtContainersChanged)
def definition(self) -> DefinitionContainer:
def getDefinition(self) -> "DefinitionContainer":
return cast(DefinitionContainer, self._containers[_ContainerIndexes.Definition])
definition = pyqtProperty(QObject, fget = getDefinition, fset = setDefinition, notify = pyqtContainersChanged)
@override(ContainerStack)
def getBottom(self) -> "DefinitionContainer":
return self.definition

View file

@ -0,0 +1,130 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, List, Optional, TYPE_CHECKING
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from UM.Settings.SettingFunction import SettingFunction
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
from cura.Settings.CuraContainerStack import CuraContainerStack
#
# This class contains all Cura-related custom functions that can be used in formulas. Some functions requires
# information such as the currently active machine, so this is made into a class instead of standalone functions.
#
class CuraFormulaFunctions:
def __init__(self, application: "CuraApplication") -> None:
self._application = application
# ================
# Custom Functions
# ================
# Gets the default extruder position of the currently active machine.
def getDefaultExtruderPosition(self) -> str:
machine_manager = self._application.getMachineManager()
return machine_manager.defaultExtruderPosition
# Gets the given setting key from the given extruder position.
def getValueInExtruder(self, extruder_position: int, property_key: str,
context: Optional["PropertyEvaluationContext"] = None) -> Any:
machine_manager = self._application.getMachineManager()
if extruder_position == -1:
extruder_position = int(machine_manager.defaultExtruderPosition)
global_stack = machine_manager.activeMachine
extruder_stack = global_stack.extruders[str(extruder_position)]
value = extruder_stack.getRawProperty(property_key, "value", context = context)
if isinstance(value, SettingFunction):
value = value(extruder_stack, context = context)
return value
# Gets all extruder values as a list for the given property.
def getValuesInAllExtruders(self, property_key: str,
context: Optional["PropertyEvaluationContext"] = None) -> List[Any]:
machine_manager = self._application.getMachineManager()
extruder_manager = self._application.getExtruderManager()
global_stack = machine_manager.activeMachine
result = []
for extruder in extruder_manager.getActiveExtruderStacks():
if not extruder.isEnabled:
continue
# only include values from extruders that are "active" for the current machine instance
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value", context = context):
continue
value = extruder.getRawProperty(property_key, "value", context = context)
if value is None:
continue
if isinstance(value, SettingFunction):
value = value(extruder, context = context)
result.append(value)
if not result:
result.append(global_stack.getProperty(property_key, "value", context = context))
return result
# Get the resolve value or value for a given key.
def getResolveOrValue(self, property_key: str, context: Optional["PropertyEvaluationContext"] = None) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
resolved_value = global_stack.getProperty(property_key, "value", context = context)
return resolved_value
# Gets the default setting value from given extruder position. The default value is what excludes the values in
# the user_changes container.
def getDefaultValueInExtruder(self, extruder_position: int, property_key: str) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
extruder_stack = global_stack.extruders[str(extruder_position)]
context = self.createContextForDefaultValueEvaluation(extruder_stack)
return self.getValueInExtruder(extruder_position, property_key, context = context)
# Gets all default setting values as a list from all extruders of the currently active machine.
# The default values are those excluding the values in the user_changes container.
def getDefaultValuesInAllExtruders(self, property_key: str) -> List[Any]:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
context = self.createContextForDefaultValueEvaluation(global_stack)
return self.getValuesInAllExtruders(property_key, context = context)
# Gets the resolve value or value for a given key without looking the first container (user container).
def getDefaultResolveOrValue(self, property_key: str) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
context = self.createContextForDefaultValueEvaluation(global_stack)
return self.getResolveOrValue(property_key, context = context)
# Creates a context for evaluating default values (skip the user_changes container).
def createContextForDefaultValueEvaluation(self, source_stack: "CuraContainerStack") -> "PropertyEvaluationContext":
context = PropertyEvaluationContext(source_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = {
"extruderValue": self.getDefaultValueInExtruder,
"extruderValues": self.getDefaultValuesInAllExtruders,
"resolveOrValue": self.getDefaultResolveOrValue,
}
return context

View file

@ -129,7 +129,7 @@ class CuraStackBuilder:
# get material container for extruders
material_container = application.empty_material_container
material_node = material_manager.getDefaultMaterial(global_stack, extruder_position, extruder_variant_name,
material_node = material_manager.getDefaultMaterial(global_stack, str(extruder_position), extruder_variant_name,
extruder_definition = extruder_definition)
if material_node and material_node.getContainer():
material_container = material_node.getContainer()
@ -145,7 +145,6 @@ class CuraStackBuilder:
quality_container = application.empty_quality_container
)
new_extruder.setNextStack(global_stack)
global_stack.addExtruder(new_extruder)
registry.addContainer(new_extruder)

View file

@ -12,11 +12,9 @@ from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack
@ -38,9 +36,12 @@ class ExtruderManager(QObject):
self._application = cura.CuraApplication.CuraApplication.getInstance()
# Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders.
self._extruder_trains = {} # type: Dict[str, Dict[str, ExtruderStack]]
self._extruder_trains = {} # type: Dict[str, Dict[str, "ExtruderStack"]]
self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack
self._selected_object_extruders = [] # type: List[str]
# TODO; I have no idea why this is a union of ID's and extruder stacks. This needs to be fixed at some point.
self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
self._addCurrentMachineExtruders()
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
@ -66,16 +67,6 @@ class ExtruderManager(QObject):
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
return None
## Return extruder count according to extruder trains.
@pyqtProperty(int, notify = extrudersChanged)
def extruderCount(self) -> int:
if not self._application.getGlobalContainerStack():
return 0 # No active machine, so no extruders.
try:
return len(self._extruder_trains[self._application.getGlobalContainerStack().getId()])
except KeyError:
return 0
## Gets a dict with the extruder stack ids with the extruder number as the key.
@pyqtProperty("QVariantMap", notify = extrudersChanged)
def extruderIds(self) -> Dict[str, str]:
@ -114,7 +105,7 @@ class ExtruderManager(QObject):
## Provides a list of extruder IDs used by the current selected objects.
@pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged)
def selectedObjectExtruders(self) -> List[str]:
def selectedObjectExtruders(self) -> List[Union[str, "ExtruderStack"]]:
if not self._selected_object_extruders:
object_extruders = set()
@ -139,7 +130,7 @@ class ExtruderManager(QObject):
elif current_extruder_trains:
object_extruders.add(current_extruder_trains[0].getId())
self._selected_object_extruders = list(object_extruders)
self._selected_object_extruders = list(object_extruders) # type: List[Union[str, "ExtruderStack"]]
return self._selected_object_extruders
@ -148,7 +139,7 @@ class ExtruderManager(QObject):
# This will trigger a recalculation of the extruders used for the
# selection.
def resetSelectedObjectExtruders(self) -> None:
self._selected_object_extruders = []
self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
self.selectedObjectExtrudersChanged.emit()
@pyqtSlot(result = QObject)
@ -383,86 +374,6 @@ class ExtruderManager(QObject):
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
extruder_stack_0.definition = extruder_definition
## Get all extruder values for a certain setting.
#
# This is exposed to SettingFunction so it can be used in value functions.
#
# \param key The key of the setting to retrieve values for.
#
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
# If no extruder has the value, the list will contain the global value.
@staticmethod
def getExtruderValues(key: str) -> List[Any]:
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) #We know that there must be a global stack by the time you're requesting setting values.
result = []
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
if not extruder.isEnabled:
continue
# only include values from extruders that are "active" for the current machine instance
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value"):
continue
value = extruder.getRawProperty(key, "value")
if value is None:
continue
if isinstance(value, SettingFunction):
value = value(extruder)
result.append(value)
if not result:
result.append(global_stack.getProperty(key, "value"))
return result
## Get all extruder values for a certain setting. This function will skip the user settings container.
#
# This is exposed to SettingFunction so it can be used in value functions.
#
# \param key The key of the setting to retrieve values for.
#
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
# If no extruder has the value, the list will contain the global value.
@staticmethod
def getDefaultExtruderValues(key: str) -> List[Any]:
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) #We know that there must be a global stack by the time you're requesting setting values.
context = PropertyEvaluationContext(global_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = {
"extruderValue": ExtruderManager.getDefaultExtruderValue,
"extruderValues": ExtruderManager.getDefaultExtruderValues,
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
}
result = []
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
# only include values from extruders that are "active" for the current machine instance
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value", context = context):
continue
value = extruder.getRawProperty(key, "value", context = context)
if value is None:
continue
if isinstance(value, SettingFunction):
value = value(extruder, context = context)
result.append(value)
if not result:
result.append(global_stack.getProperty(key, "value", context = context))
return result
## Return the default extruder position from the machine manager
@staticmethod
def getDefaultExtruderPosition() -> str:
return cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition
## Get all extruder values for a certain setting.
#
# This is exposed to qml for display purposes
@ -471,62 +382,8 @@ class ExtruderManager(QObject):
#
# \return String representing the extruder values
@pyqtSlot(str, result="QVariant")
def getInstanceExtruderValues(self, key) -> List:
return ExtruderManager.getExtruderValues(key)
## Get the value for a setting from a specific extruder.
#
# This is exposed to SettingFunction to use in value functions.
#
# \param extruder_index The index of the extruder to get the value from.
# \param key The key of the setting to get the value of.
#
# \return The value of the setting for the specified extruder or for the
# global stack if not found.
@staticmethod
def getExtruderValue(extruder_index: int, key: str) -> Any:
if extruder_index == -1:
extruder_index = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
if extruder:
value = extruder.getRawProperty(key, "value")
if isinstance(value, SettingFunction):
value = value(extruder)
else:
# Just a value from global.
value = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()).getProperty(key, "value")
return value
## Get the default value from the given extruder. This function will skip the user settings container.
#
# This is exposed to SettingFunction to use in value functions.
#
# \param extruder_index The index of the extruder to get the value from.
# \param key The key of the setting to get the value of.
#
# \return The value of the setting for the specified extruder or for the
# global stack if not found.
@staticmethod
def getDefaultExtruderValue(extruder_index: int, key: str) -> Any:
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
context = PropertyEvaluationContext(extruder)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = {
"extruderValue": ExtruderManager.getDefaultExtruderValue,
"extruderValues": ExtruderManager.getDefaultExtruderValues,
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
}
if extruder:
value = extruder.getRawProperty(key, "value", context = context)
if isinstance(value, SettingFunction):
value = value(extruder, context = context)
else: # Just a value from global.
value = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()).getProperty(key, "value", context = context)
return value
def getInstanceExtruderValues(self, key: str) -> List:
return self._application.getCuraFormulaFunctions().getValuesInAllExtruders(key)
## Get the resolve value or value for a given key
#
@ -542,28 +399,6 @@ class ExtruderManager(QObject):
return resolved_value
## Get the resolve value or value for a given key without looking the first container (user container)
#
# This is the effective value for a given key, it is used for values in the global stack.
# This is exposed to SettingFunction to use in value functions.
# \param key The key of the setting to get the value of.
#
# \return The effective value
@staticmethod
def getDefaultResolveOrValue(key: str) -> Any:
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack())
context = PropertyEvaluationContext(global_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = {
"extruderValue": ExtruderManager.getDefaultExtruderValue,
"extruderValues": ExtruderManager.getDefaultExtruderValues,
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
}
resolved_value = global_stack.getProperty(key, "value", context = context)
return resolved_value
__instance = None # type: ExtruderManager
@classmethod

View file

@ -65,16 +65,33 @@ class ExtruderStack(CuraContainerStack):
def getLoadingPriority(cls) -> int:
return 3
compatibleMaterialDiameterChanged = pyqtSignal()
## Return the filament diameter that the machine requires.
#
# If the machine has no requirement for the diameter, -1 is returned.
# \return The filament diameter for the printer
@property
def materialDiameter(self) -> float:
def getCompatibleMaterialDiameter(self) -> float:
context = PropertyEvaluationContext(self)
context.context["evaluate_from_container_index"] = _ContainerIndexes.Variant
return self.getProperty("material_diameter", "value", context = context)
return float(self.getProperty("material_diameter", "value", context = context))
def setCompatibleMaterialDiameter(self, value: float) -> None:
old_approximate_diameter = self.getApproximateMaterialDiameter()
if self.getCompatibleMaterialDiameter() != value:
self.definitionChanges.setProperty("material_diameter", "value", value)
self.compatibleMaterialDiameterChanged.emit()
# Emit approximate diameter changed signal if needed
if old_approximate_diameter != self.getApproximateMaterialDiameter():
self.approximateMaterialDiameterChanged.emit()
compatibleMaterialDiameter = pyqtProperty(float, fset = setCompatibleMaterialDiameter,
fget = getCompatibleMaterialDiameter,
notify = compatibleMaterialDiameterChanged)
approximateMaterialDiameterChanged = pyqtSignal()
## Return the approximate filament diameter that the machine requires.
#
@ -84,9 +101,11 @@ class ExtruderStack(CuraContainerStack):
# If the machine has no requirement for the diameter, -1 is returned.
#
# \return The approximate filament diameter for the printer
@pyqtProperty(float)
def approximateMaterialDiameter(self) -> float:
return round(float(self.materialDiameter))
def getApproximateMaterialDiameter(self) -> float:
return round(self.getCompatibleMaterialDiameter())
approximateMaterialDiameter = pyqtProperty(float, fget = getApproximateMaterialDiameter,
notify = approximateMaterialDiameterChanged)
## Overridden from ContainerStack
#

View file

@ -4,7 +4,7 @@
from collections import defaultdict
import threading
from typing import Any, Dict, Optional, Set, TYPE_CHECKING
from PyQt5.QtCore import pyqtProperty
from PyQt5.QtCore import pyqtProperty, pyqtSlot
from UM.Decorators import override
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
@ -13,6 +13,8 @@ from UM.Settings.SettingInstance import InstanceState
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import PropertyEvaluationContext
from UM.Logger import Logger
from UM.Resources import Resources
from UM.Platform import Platform
from UM.Util import parseBool
import cura.CuraApplication
@ -23,6 +25,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):
@ -199,6 +202,31 @@ class GlobalStack(CuraContainerStack):
def getHasMachineQuality(self) -> bool:
return parseBool(self.getMetaDataEntry("has_machine_quality", False))
## Get default firmware file name if one is specified in the firmware
@pyqtSlot(result = str)
def getDefaultFirmwareName(self) -> str:
machine_has_heated_bed = self.getProperty("machine_heated_bed", "value")
baudrate = 250000
if Platform.isLinux():
# Linux prefers a baudrate of 115200 here because older versions of
# pySerial did not support a baudrate of 250000
baudrate = 115200
# If a firmware file is available, it should be specified in the definition for the printer
hex_file = self.getMetaDataEntry("firmware_file", None)
if machine_has_heated_bed:
hex_file = self.getMetaDataEntry("firmware_hbk_file", hex_file)
if not hex_file:
Logger.log("w", "There is no firmware for machine %s.", self.getBottom().id)
return ""
try:
return Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
except FileNotFoundError:
Logger.log("w", "Firmware file %s not found.", hex_file)
return ""
## private:
global_stack_mime = MimeType(

View file

@ -20,7 +20,6 @@ from UM.Message import Message
from UM.Settings.SettingFunction import SettingFunction
from UM.Signal import postponeSignals, CompressTechnique
import cura.CuraApplication
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from cura.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
@ -29,6 +28,9 @@ from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.cura_empty_instance_containers import (empty_definition_changes_container, empty_variant_container,
empty_material_container, empty_quality_container,
empty_quality_changes_container)
from .CuraStackBuilder import CuraStackBuilder
@ -36,6 +38,7 @@ from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
from cura.Settings.CuraContainerStack import CuraContainerStack
from cura.Settings.GlobalStack import GlobalStack
from cura.Machines.MaterialManager import MaterialManager
@ -47,7 +50,7 @@ if TYPE_CHECKING:
class MachineManager(QObject):
def __init__(self, parent: QObject = None) -> None:
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._active_container_stack = None # type: Optional[ExtruderStack]
@ -66,9 +69,10 @@ class MachineManager(QObject):
self._instance_container_timer.setSingleShot(True)
self._instance_container_timer.timeout.connect(self.__emitChangedSignals)
self._application = cura.CuraApplication.CuraApplication.getInstance() #type: cura.CuraApplication.CuraApplication
self._application = application
self._container_registry = self._application.getContainerRegistry()
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._application.getContainerRegistry().containerLoadComplete.connect(self._onContainersChanged)
self._container_registry.containerLoadComplete.connect(self._onContainersChanged)
## When the global container is changed, active material probably needs to be updated.
self.globalContainerChanged.connect(self.activeMaterialChanged)
@ -80,13 +84,6 @@ class MachineManager(QObject):
self._stacks_have_errors = None # type: Optional[bool]
self._empty_container = CuraContainerRegistry.getInstance().getEmptyInstanceContainer() #type: InstanceContainer
self._empty_definition_changes_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_definition_changes")[0] #type: InstanceContainer
self._empty_variant_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_variant")[0] #type: InstanceContainer
self._empty_material_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_material")[0] #type: InstanceContainer
self._empty_quality_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_quality")[0] #type: InstanceContainer
self._empty_quality_changes_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_quality_changes")[0] #type: InstanceContainer
self._onGlobalContainerChanged()
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
@ -192,19 +189,21 @@ class MachineManager(QObject):
for extruder in self._global_container_stack.extruders.values():
extruder_configuration = ExtruderConfigurationModel()
# For compare just the GUID is needed at this moment
mat_type = extruder.material.getMetaDataEntry("material") if extruder.material != self._empty_material_container else None
mat_guid = extruder.material.getMetaDataEntry("GUID") if extruder.material != self._empty_material_container else None
mat_color = extruder.material.getMetaDataEntry("color_name") if extruder.material != self._empty_material_container else None
mat_brand = extruder.material.getMetaDataEntry("brand") if extruder.material != self._empty_material_container else None
mat_name = extruder.material.getMetaDataEntry("name") if extruder.material != self._empty_material_container else None
mat_type = extruder.material.getMetaDataEntry("material") if extruder.material != empty_material_container else None
mat_guid = extruder.material.getMetaDataEntry("GUID") if extruder.material != empty_material_container else None
mat_color = extruder.material.getMetaDataEntry("color_name") if extruder.material != empty_material_container else None
mat_brand = extruder.material.getMetaDataEntry("brand") if extruder.material != empty_material_container else None
mat_name = extruder.material.getMetaDataEntry("name") if extruder.material != empty_material_container else None
material_model = MaterialOutputModel(mat_guid, mat_type, mat_color, mat_brand, mat_name)
extruder_configuration.position = int(extruder.getMetaDataEntry("position"))
extruder_configuration.material = material_model
extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != self._empty_variant_container else None
extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != empty_variant_container else None
self._current_printer_configuration.extruderConfigurations.append(extruder_configuration)
self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != self._empty_variant_container else None
# an empty build plate configuration from the network printer is presented as an empty string, so use "" for an
# empty build plate.
self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != empty_variant_container else ""
self.currentConfigurationChanged.emit()
@pyqtSlot(QObject, result = bool)
@ -258,14 +257,14 @@ class MachineManager(QObject):
# Global stack can have only a variant if it is a buildplate
global_variant = self._global_container_stack.variant
if global_variant != self._empty_variant_container:
if global_variant != empty_variant_container:
if global_variant.getMetaDataEntry("hardware_type") != "buildplate":
self._global_container_stack.setVariant(self._empty_variant_container)
self._global_container_stack.setVariant(empty_variant_container)
# set the global material to empty as we now use the extruder stack at all times - CURA-4482
global_material = self._global_container_stack.material
if global_material != self._empty_material_container:
self._global_container_stack.setMaterial(self._empty_material_container)
if global_material != empty_material_container:
self._global_container_stack.setMaterial(empty_material_container)
# Listen for changes on all extruder stacks
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
@ -367,6 +366,10 @@ class MachineManager(QObject):
return
global_stack = containers[0]
# Make sure that the default machine actions for this machine have been added
self._application.getMachineActionManager().addDefaultMachineActions(global_stack)
ExtruderManager.getInstance()._fixSingleExtrusionMachineExtruderDefinition(global_stack)
if not global_stack.isValid():
# Mark global stack as invalid
@ -593,7 +596,7 @@ class MachineManager(QObject):
def globalVariantName(self) -> str:
if self._global_container_stack:
variant = self._global_container_stack.variant
if variant and not isinstance(variant, type(self._empty_variant_container)):
if variant and not isinstance(variant, type(empty_variant_container)):
return variant.getName()
return ""
@ -781,7 +784,7 @@ class MachineManager(QObject):
if not stack.isEnabled:
continue
material_container = stack.material
if material_container == self._empty_material_container:
if material_container == empty_material_container:
continue
if material_container.getMetaDataEntry("buildplate_compatible"):
buildplate_compatible = buildplate_compatible and material_container.getMetaDataEntry("buildplate_compatible")[self.activeVariantBuildplateName]
@ -803,7 +806,7 @@ class MachineManager(QObject):
extruder_stacks = self._global_container_stack.extruders.values()
for stack in extruder_stacks:
material_container = stack.material
if material_container == self._empty_material_container:
if material_container == empty_material_container:
continue
buildplate_compatible = material_container.getMetaDataEntry("buildplate_compatible")[self.activeVariantBuildplateName] if material_container.getMetaDataEntry("buildplate_compatible") else True
buildplate_usable = material_container.getMetaDataEntry("buildplate_recommended")[self.activeVariantBuildplateName] if material_container.getMetaDataEntry("buildplate_recommended") else True
@ -873,7 +876,7 @@ class MachineManager(QObject):
extruder_manager = self._application.getExtruderManager()
definition_changes_container = self._global_container_stack.definitionChanges
if not self._global_container_stack or definition_changes_container == self._empty_definition_changes_container:
if not self._global_container_stack or definition_changes_container == empty_definition_changes_container:
return
previous_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
@ -893,7 +896,11 @@ class MachineManager(QObject):
extruder_nr = node.callDecoration("getActiveExtruderPosition")
if extruder_nr is not None and int(extruder_nr) > extruder_count - 1:
node.callDecoration("setActiveExtruder", extruder_manager.getExtruderStack(extruder_count - 1).getId())
extruder = extruder_manager.getExtruderStack(extruder_count - 1)
if extruder is not None:
node.callDecoration("setActiveExtruder", extruder.getId())
else:
Logger.log("w", "Could not find extruder to set active.")
# Make sure one of the extruder stacks is active
extruder_manager.setActiveExtruderIndex(0)
@ -1068,7 +1075,7 @@ class MachineManager(QObject):
for stack in active_stacks:
variant_container = stack.variant
position = stack.getMetaDataEntry("position")
if variant_container and variant_container != self._empty_variant_container:
if variant_container and variant_container != empty_variant_container:
result[position] = variant_container.getName()
return result
@ -1082,11 +1089,11 @@ class MachineManager(QObject):
return
self._current_quality_group = None
self._current_quality_changes_group = None
self._global_container_stack.quality = self._empty_quality_container
self._global_container_stack.qualityChanges = self._empty_quality_changes_container
self._global_container_stack.quality = empty_quality_container
self._global_container_stack.qualityChanges = empty_quality_changes_container
for extruder in self._global_container_stack.extruders.values():
extruder.quality = self._empty_quality_container
extruder.qualityChanges = self._empty_quality_changes_container
extruder.quality = empty_quality_container
extruder.qualityChanges = empty_quality_changes_container
self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit()
@ -1111,13 +1118,13 @@ class MachineManager(QObject):
# Set quality and quality_changes for the GlobalStack
self._global_container_stack.quality = quality_group.node_for_global.getContainer()
if empty_quality_changes:
self._global_container_stack.qualityChanges = self._empty_quality_changes_container
self._global_container_stack.qualityChanges = empty_quality_changes_container
# Set quality and quality_changes for each ExtruderStack
for position, node in quality_group.nodes_for_extruders.items():
self._global_container_stack.extruders[str(position)].quality = node.getContainer()
if empty_quality_changes:
self._global_container_stack.extruders[str(position)].qualityChanges = self._empty_quality_changes_container
self._global_container_stack.extruders[str(position)].qualityChanges = empty_quality_changes_container
self.activeQualityGroupChanged.emit()
self.activeQualityChangesGroupChanged.emit()
@ -1143,8 +1150,8 @@ class MachineManager(QObject):
if quality_group is None:
self._fixQualityChangesGroupToNotSupported(quality_changes_group)
quality_changes_container = self._empty_quality_changes_container
quality_container = self._empty_quality_container
quality_changes_container = empty_quality_changes_container
quality_container = 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():
@ -1159,8 +1166,8 @@ class MachineManager(QObject):
if quality_group is not None:
quality_node = quality_group.nodes_for_extruders.get(position)
quality_changes_container = self._empty_quality_changes_container
quality_container = self._empty_quality_container
quality_changes_container = empty_quality_changes_container
quality_container = empty_quality_container
if quality_changes_node and quality_changes_node.getContainer():
quality_changes_container = cast(InstanceContainer, quality_changes_node.getContainer())
if quality_node and quality_node.getContainer():
@ -1194,7 +1201,7 @@ class MachineManager(QObject):
self._global_container_stack.extruders[position].material = container_node.getContainer()
root_material_id = container_node.getMetaDataEntry("base_file", None)
else:
self._global_container_stack.extruders[position].material = self._empty_material_container
self._global_container_stack.extruders[position].material = empty_material_container
root_material_id = None
# The _current_root_material_id is used in the MaterialMenu to see which material is selected
if root_material_id != self._current_root_material_id[position]:
@ -1269,14 +1276,10 @@ class MachineManager(QObject):
current_material_base_name = extruder.material.getMetaDataEntry("base_file")
current_nozzle_name = None
if extruder.variant.getId() != self._empty_variant_container.getId():
if extruder.variant.getId() != empty_variant_container.getId():
current_nozzle_name = extruder.variant.getMetaDataEntry("name")
from UM.Settings.Interfaces import PropertyEvaluationContext
from cura.Settings.CuraContainerStack import _ContainerIndexes
context = PropertyEvaluationContext(extruder)
context.context["evaluate_from_container_index"] = _ContainerIndexes.DefinitionChanges
material_diameter = extruder.getProperty("material_diameter", "value", context)
material_diameter = extruder.getCompatibleMaterialDiameter()
candidate_materials = self._material_manager.getAvailableMaterials(
self._global_container_stack.definition,
current_nozzle_name,
@ -1344,12 +1347,12 @@ class MachineManager(QObject):
if variant_container_node:
self._setVariantNode(position, variant_container_node)
else:
self._global_container_stack.extruders[position].variant = self._empty_variant_container
self._global_container_stack.extruders[position].variant = empty_variant_container
if material_container_node:
self._setMaterial(position, material_container_node)
else:
self._global_container_stack.extruders[position].material = self._empty_material_container
self._global_container_stack.extruders[position].material = empty_material_container
self.updateMaterialWithVariant(position)
if configuration.buildplateConfiguration is not None:
@ -1357,9 +1360,9 @@ class MachineManager(QObject):
if global_variant_container_node:
self._setGlobalVariant(global_variant_container_node)
else:
self._global_container_stack.variant = self._empty_variant_container
self._global_container_stack.variant = empty_variant_container
else:
self._global_container_stack.variant = self._empty_variant_container
self._global_container_stack.variant = empty_variant_container
self._updateQualityWithMaterial()
# See if we need to show the Discard or Keep changes screen
@ -1411,7 +1414,7 @@ class MachineManager(QObject):
position = str(position)
extruder_stack = self._global_container_stack.extruders[position]
nozzle_name = extruder_stack.variant.getName()
material_diameter = extruder_stack.approximateMaterialDiameter
material_diameter = extruder_stack.getApproximateMaterialDiameter()
material_node = self._material_manager.getMaterialNode(machine_definition_id, nozzle_name, buildplate_name,
material_diameter, root_material_id)
self.setMaterial(position, material_node)
@ -1477,7 +1480,7 @@ class MachineManager(QObject):
# This is not changing the quality for the active machine !!!!!!!!
global_stack.quality = quality_group.node_for_global.getContainer()
for extruder_nr, extruder_stack in global_stack.extruders.items():
quality_container = self._empty_quality_container
quality_container = empty_quality_container
if extruder_nr in quality_group.nodes_for_extruders:
container = quality_group.nodes_for_extruders[extruder_nr].getContainer()
quality_container = container if container is not None else quality_container
@ -1521,7 +1524,7 @@ class MachineManager(QObject):
@pyqtProperty(str, notify = activeQualityGroupChanged)
def activeQualityOrQualityChangesName(self) -> str:
name = self._empty_quality_container.getName()
name = empty_quality_container.getName()
if self._current_quality_changes_group:
name = self._current_quality_changes_group.name
elif self._current_quality_group:

View file

@ -35,10 +35,9 @@ class MachineNameValidator(QObject):
## Check if a specified machine name is allowed.
#
# \param name The machine name to check.
# \param position The current position of the cursor in the text box.
# \return ``QValidator.Invalid`` if it's disallowed, or
# ``QValidator.Acceptable`` if it's allowed.
def validate(self, name, position):
def validate(self, name):
#Check for file name length of the current settings container (which is the longest file we're saving with the name).
try:
filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax
@ -54,7 +53,7 @@ class MachineNameValidator(QObject):
## Updates the validation state of a machine name text field.
@pyqtSlot(str)
def updateValidation(self, new_name):
is_valid = self.validate(new_name, 0)
is_valid = self.validate(new_name)
if is_valid == QValidator.Acceptable:
self.validation_regex = "^.*$" #Matches anything.
else:

View file

@ -1,6 +1,6 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import List
from typing import List, Optional, TYPE_CHECKING
from PyQt5.QtCore import QObject, QTimer, pyqtProperty, pyqtSignal
from UM.FlameProfiler import pyqtSlot
@ -20,13 +20,18 @@ from UM.Settings.SettingInstance import InstanceState
from cura.Settings.ExtruderManager import ExtruderManager
if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack
from UM.Settings.SettingDefinition import SettingDefinition
class SettingInheritanceManager(QObject):
def __init__(self, parent = None):
def __init__(self, parent = None) -> None:
super().__init__(parent)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._global_container_stack = None
self._settings_with_inheritance_warning = []
self._active_container_stack = None
self._global_container_stack = None # type: Optional[ContainerStack]
self._settings_with_inheritance_warning = [] # type: List[str]
self._active_container_stack = None # type: Optional[ExtruderStack]
self._onGlobalContainerChanged()
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
@ -41,7 +46,9 @@ class SettingInheritanceManager(QObject):
## Get the keys of all children settings with an override.
@pyqtSlot(str, result = "QStringList")
def getChildrenKeysWithOverride(self, key):
def getChildrenKeysWithOverride(self, key: str) -> List[str]:
if self._global_container_stack is None:
return []
definitions = self._global_container_stack.definition.findDefinitions(key=key)
if not definitions:
Logger.log("w", "Could not find definition for key [%s]", key)
@ -53,9 +60,11 @@ class SettingInheritanceManager(QObject):
return result
@pyqtSlot(str, str, result = "QStringList")
def getOverridesForExtruder(self, key, extruder_index):
result = []
def getOverridesForExtruder(self, key: str, extruder_index: str) -> List[str]:
if self._global_container_stack is None:
return []
result = [] # type: List[str]
extruder_stack = ExtruderManager.getInstance().getExtruderStack(extruder_index)
if not extruder_stack:
Logger.log("w", "Unable to find extruder for current machine with index %s", extruder_index)
@ -73,16 +82,16 @@ class SettingInheritanceManager(QObject):
return result
@pyqtSlot(str)
def manualRemoveOverride(self, key):
def manualRemoveOverride(self, key: str) -> None:
if key in self._settings_with_inheritance_warning:
self._settings_with_inheritance_warning.remove(key)
self.settingsWithIntheritanceChanged.emit()
@pyqtSlot()
def forceUpdate(self):
def forceUpdate(self) -> None:
self._update()
def _onActiveExtruderChanged(self):
def _onActiveExtruderChanged(self) -> None:
new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack()
if not new_active_stack:
self._active_container_stack = None
@ -94,13 +103,14 @@ class SettingInheritanceManager(QObject):
self._active_container_stack.containersChanged.disconnect(self._onContainersChanged)
self._active_container_stack = new_active_stack
self._active_container_stack.propertyChanged.connect(self._onPropertyChanged)
self._active_container_stack.containersChanged.connect(self._onContainersChanged)
if self._active_container_stack is not None:
self._active_container_stack.propertyChanged.connect(self._onPropertyChanged)
self._active_container_stack.containersChanged.connect(self._onContainersChanged)
self._update() # Ensure that the settings_with_inheritance_warning list is populated.
def _onPropertyChanged(self, key, property_name):
def _onPropertyChanged(self, key: str, property_name: str) -> None:
if (property_name == "value" or property_name == "enabled") and self._global_container_stack:
definitions = self._global_container_stack.definition.findDefinitions(key = key)
definitions = self._global_container_stack.definition.findDefinitions(key = key) # type: List["SettingDefinition"]
if not definitions:
return
@ -139,7 +149,7 @@ class SettingInheritanceManager(QObject):
if settings_with_inheritance_warning_changed:
self.settingsWithIntheritanceChanged.emit()
def _recursiveCheck(self, definition):
def _recursiveCheck(self, definition: "SettingDefinition") -> bool:
for child in definition.children:
if child.key in self._settings_with_inheritance_warning:
return True
@ -149,7 +159,7 @@ class SettingInheritanceManager(QObject):
return False
@pyqtProperty("QVariantList", notify = settingsWithIntheritanceChanged)
def settingsWithInheritanceWarning(self):
def settingsWithInheritanceWarning(self) -> List[str]:
return self._settings_with_inheritance_warning
## Check if a setting has an inheritance function that is overwritten
@ -157,9 +167,14 @@ class SettingInheritanceManager(QObject):
has_setting_function = False
if not stack:
stack = self._active_container_stack
if not stack: #No active container stack yet!
if not stack: # No active container stack yet!
return False
containers = [] # type: List[ContainerInterface]
if self._active_container_stack is None:
return False
all_keys = self._active_container_stack.getAllKeys()
containers = [] # type: List[ContainerInterface]
## Check if the setting has a user state. If not, it is never overwritten.
has_user_state = stack.getProperty(key, "state") == InstanceState.User
@ -190,8 +205,8 @@ class SettingInheritanceManager(QObject):
has_setting_function = isinstance(value, SettingFunction)
if has_setting_function:
for setting_key in value.getUsedSettingKeys():
if setting_key in self._active_container_stack.getAllKeys():
break # We found an actual setting. So has_setting_function can remain true
if setting_key in all_keys:
break # We found an actual setting. So has_setting_function can remain true
else:
# All of the setting_keys turned out to not be setting keys at all!
# This can happen due enum keys also being marked as settings.
@ -205,7 +220,7 @@ class SettingInheritanceManager(QObject):
break # There is a setting function somewhere, stop looking deeper.
return has_setting_function and has_non_function_value
def _update(self):
def _update(self) -> None:
self._settings_with_inheritance_warning = [] # Reset previous data.
# Make sure that the GlobalStack is not None. sometimes the globalContainerChanged signal gets here late.
@ -226,7 +241,7 @@ class SettingInheritanceManager(QObject):
# Notify others that things have changed.
self.settingsWithIntheritanceChanged.emit()
def _onGlobalContainerChanged(self):
def _onGlobalContainerChanged(self) -> None:
if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
self._global_container_stack.containersChanged.disconnect(self._onContainersChanged)

View file

@ -0,0 +1,90 @@
import os
import urllib.parse
from configparser import ConfigParser
from typing import List
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
from UM.Logger import Logger
from UM.MimeTypeDatabase import MimeTypeDatabase
class SettingVisibilityPreset(QObject):
onSettingsChanged = pyqtSignal()
onNameChanged = pyqtSignal()
onWeightChanged = pyqtSignal()
onIdChanged = pyqtSignal()
def __init__(self, preset_id: str = "", name: str = "", weight: int = 0, parent = None) -> None:
super().__init__(parent)
self._settings = [] # type: List[str]
self._id = preset_id
self._weight = weight
self._name = name
@pyqtProperty("QStringList", notify = onSettingsChanged)
def settings(self) -> List[str]:
return self._settings
@pyqtProperty(str, notify = onIdChanged)
def presetId(self) -> str:
return self._id
@pyqtProperty(int, notify = onWeightChanged)
def weight(self) -> int:
return self._weight
@pyqtProperty(str, notify = onNameChanged)
def name(self) -> str:
return self._name
def setName(self, name: str) -> None:
if name != self._name:
self._name = name
self.onNameChanged.emit()
def setId(self, id: str) -> None:
if id != self._id:
self._id = id
self.onIdChanged.emit()
def setWeight(self, weight: int) -> None:
if weight != self._weight:
self._weight = weight
self.onWeightChanged.emit()
def setSettings(self, settings: List[str]) -> None:
if set(settings) != set(self._settings):
self._settings = list(set(settings)) # filter out non unique
self.onSettingsChanged.emit()
# Load a preset from file. We expect a file that can be parsed by means of the config parser.
# The sections indicate the categories and the parameters placed in it (which don't need values) are the settings
# that should be considered visible.
def loadFromFile(self, file_path: str) -> None:
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
item_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_path)))
if not os.path.isfile(file_path):
Logger.log("e", "[%s] is not a file", file_path)
return None
parser = ConfigParser(interpolation = None, allow_no_value = True) # Accept options without any value,
parser.read([file_path])
if not parser.has_option("general", "name") or not parser.has_option("general", "weight"):
return None
settings = [] # type: List[str]
for section in parser.sections():
if section == "general":
continue
settings.append(section)
for option in parser[section].keys():
settings.append(option)
self.setSettings(settings)
self.setId(item_id)
self.setName(parser["general"]["name"])
self.setWeight(int(parser["general"]["weight"]))

View file

@ -1,15 +1,17 @@
from UM.Qt.ListModel import ListModel
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from collections import OrderedDict
from PyQt5.QtCore import pyqtSlot, Qt
from UM.Application import Application
from cura.Settings.ExtruderManager import ExtruderManager
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog
from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from collections import OrderedDict
import os
from UM.Qt.ListModel import ListModel
class UserChangesModel(ListModel):
@ -38,9 +40,13 @@ class UserChangesModel(ListModel):
self._update()
def _update(self):
application = Application.getInstance()
machine_manager = application.getMachineManager()
cura_formula_functions = application.getCuraFormulaFunctions()
item_dict = OrderedDict()
item_list = []
global_stack = Application.getInstance().getGlobalContainerStack()
global_stack = machine_manager.activeMachine
if not global_stack:
return
@ -71,13 +77,7 @@ class UserChangesModel(ListModel):
# Override "getExtruderValue" with "getDefaultExtruderValue" so we can get the default values
user_changes = containers.pop(0)
default_value_resolve_context = PropertyEvaluationContext(stack)
default_value_resolve_context.context["evaluate_from_container_index"] = 1 # skip the user settings container
default_value_resolve_context.context["override_operators"] = {
"extruderValue": ExtruderManager.getDefaultExtruderValue,
"extruderValues": ExtruderManager.getDefaultExtruderValues,
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
}
default_value_resolve_context = cura_formula_functions.createContextForDefaultValueEvaluation(stack)
for setting_key in user_changes.getAllKeys():
original_value = None

View file

@ -298,7 +298,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
values = parser["values"] if parser.has_section("values") else dict()
num_settings_overriden_by_quality_changes += len(values)
# Check if quality changes already exists.
quality_changes = self._container_registry.findInstanceContainers(id = container_id)
quality_changes = self._container_registry.findInstanceContainers(name = custom_quality_name,
type = "quality_changes")
if quality_changes:
containers_found_dict["quality_changes"] = True
# Check if there really is a conflict by comparing the values
@ -926,7 +927,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
build_plate_id = global_stack.variant.getId()
# get material diameter of this extruder
machine_material_diameter = extruder_stack.materialDiameter
machine_material_diameter = extruder_stack.getCompatibleMaterialDiameter()
material_node = material_manager.getMaterialNode(global_stack.definition.getId(),
extruder_stack.variant.getName(),
build_plate_id,
@ -934,7 +935,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
root_material_id)
if material_node is not None and material_node.getContainer() is not None:
extruder_stack.material = material_node.getContainer()
extruder_stack.material = material_node.getContainer() # type: InstanceContainer
def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
# Clear all first
@ -1012,7 +1013,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.
def _getContainerIdListFromSerialized(self, serialized):
parser = ConfigParser(interpolation=None, empty_lines_in_values=False)
parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
parser.read_string(serialized)
container_ids = []
@ -1033,7 +1034,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return container_ids
def _getMachineNameFromSerializedStack(self, serialized):
parser = ConfigParser(interpolation=None, empty_lines_in_values=False)
parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
parser.read_string(serialized)
return parser["general"].get("name", "")

View file

@ -12,7 +12,7 @@ from . import ThreeMFWorkspaceWriter
from UM.i18n import i18nCatalog
from UM.Platform import Platform
i18n_catalog = i18nCatalog("uranium")
i18n_catalog = i18nCatalog("cura")
def getMetaData():
workspace_extension = "3mf"

View file

@ -1,10 +1,83 @@
[3.5.1]
[3.6.0]
*BlackBelt changes
- Added automatic support generation
- Optimised start and end gcode
- Updated firmware to Marlin 1.1.9, with support for homing the Y axis and parking the nozzle on pause
- Fixed multiplying models on the belt
- Fixed layer view for objects that are cut off below the belt
*Gyroid infill
New infill pattern with enhanced strength properties. Gyroid infill is one of the strongest infill types for a given weight, has isotropic properties, and prints relatively fast with reduced material use and a fully connected part interior. Note: Slicing time can increase up to 40 seconds or more, depending on the model. Contributed by smartavionics.
*Support brim
New setting that integrates the first layer of support material with the brims geometry. This significantly improves adhesion when printing with support material. Contributed by BagelOrb.
*Cooling fan number
It is now possible to specify the cooling fan to use if your printer has multiple fans. This is implemented under Machine settings in the Extruder tab. Contributed by smartavionics.
*Settings refactor
The CuraEngine has been refactored to create a more testable, future-proof way of storing and representing settings. This makes slicing faster, and future development easier.
*Print core CC 0.6
The new print core CC 0.6 is selectable when the Ultimaker S5 profile is active. This print core is optimized for use with abrasive materials and composites.
*File name and layer display
Added M117 commands to GCODE to give real-time information about the print job file name and layer number shown on the printers display when printing via USB. Contributed by adecastilho.
*Firmware checker/Ultimaker S5
The update checker code has been improved and tested for more reliable firmware update notifications in Ultimaker Cura. The Ultimaker S5 is now included.
*Fullscreen mode shortcuts
Fullscreen mode can be toggled using the View menu or with the keyboard shortcuts: Command + Control + F (macOS), or F11 (Windows and Linux). Contributed by KangDroid.
*Configuration error message
In previous versions, Ultimaker Cura would display an error dialog explaining when something happened to user configuration files, including the option to reset to factory defaults. This would not warn about losing the current printer and print profile settings, so this information has been added.
*Rename Toolbox to Marketplace
The entry points to the Toolbox are now renamed to Marketplace.
*Materials in the Marketplace
A new tab has been added to the Marketplace that includes downloadable material profiles, to quickly and easily prepare models for a range of third-party materials.
*New third-party definitions
New profiles added for Anycube 4MAx and Tizyx K25. Contributed by jscurtu and ValentinPitre respectively.
*Improved definitions for Ender-3
The Ender-3 build plate size has been adjusted to the correct size of 235 x 235 mm, corrected the start-up sequence, and the printhead position has been adjusted when prints are purged or completed. Contributed by stelgenhof.
*Add mesh names to slicing message
Added comment generation to indicate which mesh the GCODE after this comment is constructing. Contributed by paukstelis.
*Bug fixes
- The active material is highlighted in Ultimaker Curas material manager list. This behavior is now consistent with the profile and machine manager.
- The option to use 1.75 mm diameter filament with third-party 3D printers is now fixed and does not revert back to 2.85 mm. This fix also applies the appropriate a Z-axis speed change for 1.75 mm filament printers. Contributed by kaleidoscopeit.
- A fix was created to handle OSX version 10.10, but due to the QT upgrade, users with older versions wont be able to run Ultimaker Cura on their system without a system update. This applies to OSX version 10.09 and 10.08.
- Fixed a memory leak when leaving the “Monitor” page open.
- Added performance improvements to the PolygonConnector to efficiently connect polygons that are close to each other. This also reduces the chances of the print head collide with previously printed things. Contributed by BagelOrb.
- Fixed a bug where the GCODE reader didnt show retractions.
- Changes the USBPrinting update thread to prevent flooding the printer with M105 temperature update requests. Contributed by fieldOfView.
- Fix the behavior of the "manage visible settings" button, when pressing the "cog" icon of a particular category. Contributed by fieldOfView.
- Add a new post processing script that pauses the print at a certain height that works with RepRap printers. Contributed by Kriechi.
- Fix updates to the print monitor temperatures while preheating. Contributed by fieldOfView.
- Fixed a bug where material cost is not shown unless weight is changed.
- Fixed bugs crashing the CuraEngine when TreeSupport is enabled.
- Fixed a bug where Ultimaker Cura would upload the wrong firmware after switching printers in the UI.
- Fixed a bug where the layer view was missing if the first layer was empty.
- Fixed a bug where erroneous combing movements were taking place.
- Fixed a bug where the initial layer temperature is set correctly for the first object but then never again.
- Fixed a bug where clicking the fx icon didnt respond.
[3.5.1]
*Bug fixes
- Fixed M104 temperature commands giving inaccurate results.
- Fixed crashes caused by loading files from USB stick on Windows platforms.
- Fixed several issues with configuration files that missed the type in the metadata.
- Fixed issues caused by skin/infill optimization.
- Fixed several issues related to missing definition files for third-party printers.
- Fixed an issue where combing path generation cuts corners.
- Fixed a range of crashes caused by lock files.
- Fixed issues with remembering save directories on MacOS.
- Fixed an issue where CuraEngine uses incorrect material settings.
- Fixed an issue where some support layers don't have support infill.
[3.5.0]

View file

@ -3,8 +3,6 @@
from . import ChangeLog
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {}

View file

@ -29,6 +29,7 @@ message Object
bytes normals = 3; //An array of 3 floats.
bytes indices = 4; //An array of ints.
repeated Setting settings = 5; // Setting override per object, overruling the global settings.
string name = 6;
}
message Progress

View file

@ -179,8 +179,7 @@ class CuraEngineBackend(QObject, Backend):
# This is useful for debugging and used to actually start the engine.
# \return list of commands and args / parameters.
def getEngineCommand(self) -> List[str]:
json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json")
command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""]
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")

View file

@ -2,6 +2,7 @@
#Cura is released under the terms of the LGPLv3 or higher.
import gc
import sys
from UM.Job import Job
from UM.Application import Application
@ -95,23 +96,35 @@ class ProcessSlicedLayersJob(Job):
layer_count = len(self._layers)
# Find the minimum layer number
# When disabling the remove empty first layers setting, the minimum layer number will be a positive
# value. In that case the first empty layers will be discarded and start processing layers from the
# first layer with data.
# When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we
# instead simply offset all other layers so the lowest layer is always 0. It could happens that
# the first raft layer has value -8 but there are just 4 raft (negative) layers.
min_layer_number = 0
# simply offset all other layers so the lowest layer is always 0. It could happens that the first
# raft layer has value -8 but there are just 4 raft (negative) layers.
min_layer_number = sys.maxsize
negative_layers = 0
for layer in self._layers:
if layer.id < min_layer_number:
min_layer_number = layer.id
if layer.id < 0:
negative_layers += 1
if layer.repeatedMessageCount("path_segment") > 0:
if layer.id < min_layer_number:
min_layer_number = layer.id
if layer.id < 0:
negative_layers += 1
current_layer = 0
for layer in self._layers:
# Negative layers are offset by the minimum layer number, but the positive layers are just
# offset by the number of negative layers so there is no layer gap between raft and model
abs_layer_number = layer.id + abs(min_layer_number) if layer.id < 0 else layer.id + negative_layers
# If the layer is below the minimum, it means that there is no data, so that we don't create a layer
# data. However, if there are empty layers in between, we compute them.
if layer.id < min_layer_number:
continue
# Layers are offset by the minimum layer number. In case the raft (negative layers) is being used,
# then the absolute layer number is adjusted by removing the empty layers that can be in between raft
# and the model
abs_layer_number = layer.id - min_layer_number
if layer.id >= 0 and negative_layers != 0:
abs_layer_number += (min_layer_number + negative_layers)
layer_data.addLayer(abs_layer_number)
this_layer = layer_data.getLayer(abs_layer_number)

View file

@ -277,7 +277,7 @@ class StartSliceJob(Job):
obj = group_message.addRepeatedMessage("objects")
obj.id = id(object)
obj.name = object.getName()
indices = mesh_data.getIndices()
if indices is not None:
flat_verts = numpy.take(verts, indices.flatten(), axis=0)
@ -447,8 +447,7 @@ class StartSliceJob(Job):
Job.yieldThread()
# Ensure that the engine is aware what the build extruder is.
if stack.getProperty("machine_extruder_count", "value") > 1:
changed_setting_keys.add("extruder_nr")
changed_setting_keys.add("extruder_nr")
# Get values for all changed settings
for key in changed_setting_keys:

View file

@ -50,7 +50,7 @@ class CuraProfileReader(ProfileReader):
# \param profile_id \type{str} The name of the profile.
# \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names.
def _upgradeProfile(self, serialized, profile_id):
parser = configparser.ConfigParser(interpolation=None)
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
if "general" not in parser:

View file

@ -1,9 +1,12 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from typing import Set
from UM.Extension import Extension
from UM.Application import Application
from UM.Logger import Logger
@ -13,6 +16,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.GlobalStack import GlobalStack
from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
i18n_catalog = i18nCatalog("cura")
@ -21,32 +25,31 @@ i18n_catalog = i18nCatalog("cura")
# The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
# to change it to work for other applications.
class FirmwareUpdateChecker(Extension):
JEDI_VERSION_URL = "http://software.ultimaker.com/jedi/releases/latest.version?utm_source=cura&utm_medium=software&utm_campaign=resources"
def __init__(self):
def __init__(self) -> None:
super().__init__()
# Initialize the Preference called `latest_checked_firmware` that stores the last version
# checked for the UM3. In the future if we need to check other printers' firmware
Application.getInstance().getPreferences().addPreference("info/latest_checked_firmware", "")
# Listen to a Signal that indicates a change in the list of printers, just if the user has enabled the
# 'check for updates' option
# "check for updates" option
Application.getInstance().getPreferences().addPreference("info/automatic_update_check", True)
if Application.getInstance().getPreferences().getValue("info/automatic_update_check"):
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
self._download_url = None
self._check_job = None
self._checked_printer_names = set() # type: Set[str]
## Callback for the message that is spawned when there is a new version.
def _onActionTriggered(self, message, action):
if action == "download":
if self._download_url is not None:
QDesktopServices.openUrl(QUrl(self._download_url))
def _onSetDownloadUrl(self, download_url):
self._download_url = download_url
if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD:
machine_id = message.getMachineId()
download_url = message.getDownloadUrl()
if download_url is not None:
if QDesktopServices.openUrl(QUrl(download_url)):
Logger.log("i", "Redirected browser to {0} to show newly available firmware.".format(download_url))
else:
Logger.log("e", "Can't reach URL: {0}".format(download_url))
else:
Logger.log("e", "Can't find URL for {0}".format(machine_id))
def _onContainerAdded(self, container):
# Only take care when a new GlobalStack was added
@ -63,13 +66,18 @@ class FirmwareUpdateChecker(Extension):
# \param silent type(boolean) Suppresses messages other than "new version found" messages.
# This is used when checking for a new firmware version at startup.
def checkFirmwareVersion(self, container = None, silent = False):
# Do not run multiple check jobs in parallel
if self._check_job is not None:
Logger.log("i", "A firmware update check is already running, do nothing.")
container_name = container.definition.getName()
if container_name in self._checked_printer_names:
return
self._checked_printer_names.add(container_name)
metadata = container.definition.getMetaData().get("firmware_update_info")
if metadata is None:
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(container_name))
return
self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent, url = self.JEDI_VERSION_URL,
callback = self._onActionTriggered,
set_download_url_callback = self._onSetDownloadUrl)
self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent,
machine_name = container_name, metadata = metadata,
callback = self._onActionTriggered)
self._check_job.start()
self._check_job.finished.connect(self._onJobFinished)

View file

@ -1,13 +1,18 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application
from UM.Message import Message
from UM.Logger import Logger
from UM.Job import Job
from UM.Version import Version
import urllib.request
import codecs
from urllib.error import URLError
from typing import Dict, Optional
from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
@ -15,46 +20,86 @@ i18n_catalog = i18nCatalog("cura")
## This job checks if there is an update available on the provided URL.
class FirmwareUpdateCheckerJob(Job):
def __init__(self, container = None, silent = False, url = None, callback = None, set_download_url_callback = None):
STRING_ZERO_VERSION = "0.0.0"
STRING_EPSILON_VERSION = "0.0.1"
ZERO_VERSION = Version(STRING_ZERO_VERSION)
EPSILON_VERSION = Version(STRING_EPSILON_VERSION)
def __init__(self, container, silent, machine_name, metadata, callback) -> None:
super().__init__()
self._container = container
self.silent = silent
self._url = url
self._callback = callback
self._set_download_url_callback = set_download_url_callback
def run(self):
if not self._url:
Logger.log("e", "Can not check for a new release. URL not set!")
return
self._machine_name = machine_name
self._metadata = metadata
self._lookups = None # type:Optional[FirmwareUpdateCheckerLookup]
self._headers = {} # type:Dict[str, str] # Don't set headers yet.
def getUrlResponse(self, url: str) -> str:
result = self.STRING_ZERO_VERSION
try:
request = urllib.request.Request(url, headers = self._headers)
response = urllib.request.urlopen(request)
result = response.read().decode("utf-8")
except URLError:
Logger.log("w", "Could not reach '{0}', if this URL is old, consider removal.".format(url))
return result
def parseVersionResponse(self, response: str) -> Version:
raw_str = response.split("\n", 1)[0].rstrip()
return Version(raw_str)
def getCurrentVersion(self) -> Version:
max_version = self.ZERO_VERSION
if self._lookups is None:
return max_version
machine_urls = self._lookups.getCheckUrls()
if machine_urls is not None:
for url in machine_urls:
version = self.parseVersionResponse(self.getUrlResponse(url))
if version > max_version:
max_version = version
if max_version < self.EPSILON_VERSION:
Logger.log("w", "MachineID {0} not handled!".format(self._lookups.getMachineName()))
return max_version
def run(self):
if self._lookups is None:
self._lookups = FirmwareUpdateCheckerLookup(self._machine_name, self._metadata)
try:
# Initialize a Preference that stores the last version checked for this printer.
Application.getInstance().getPreferences().addPreference(
getSettingsKeyForMachine(self._lookups.getMachineId()), "")
# Get headers
application_name = Application.getInstance().getApplicationName()
headers = {"User-Agent": "%s - %s" % (application_name, Application.getInstance().getVersion())}
request = urllib.request.Request(self._url, headers = headers)
current_version_file = urllib.request.urlopen(request)
reader = codecs.getreader("utf-8")
application_version = Application.getInstance().getVersion()
self._headers = {"User-Agent": "%s - %s" % (application_name, application_version)}
# get machine name from the definition container
machine_name = self._container.definition.getName()
machine_name_parts = machine_name.lower().split(" ")
# If it is not None, then we compare between the checked_version and the current_version
# Now we just do that if the active printer is Ultimaker 3 or Ultimaker 3 Extended or any
# other Ultimaker 3 that will come in the future
if len(machine_name_parts) >= 2 and machine_name_parts[:2] == ["ultimaker", "3"]:
Logger.log("i", "You have a UM3 in printer list. Let's check the firmware!")
machine_id = self._lookups.getMachineId()
if machine_id is not None:
Logger.log("i", "You have a(n) {0} in the printer list. Let's check the firmware!".format(machine_name))
# Nothing to parse, just get the string
# TODO: In the future may be done by parsing a JSON file with diferent version for each printer model
current_version = reader(current_version_file).readline().rstrip()
current_version = self.getCurrentVersion()
# If it is the first time the version is checked, the checked_version is ''
checked_version = Application.getInstance().getPreferences().getValue("info/latest_checked_firmware")
# If it is the first time the version is checked, the checked_version is ""
setting_key_str = getSettingsKeyForMachine(machine_id)
checked_version = Version(Application.getInstance().getPreferences().getValue(setting_key_str))
# If the checked_version is '', it's because is the first time we check firmware and in this case
# If the checked_version is "", it's because is the first time we check firmware and in this case
# we will not show the notification, but we will store it for the next time
Application.getInstance().getPreferences().setValue("info/latest_checked_firmware", current_version)
Application.getInstance().getPreferences().setValue(setting_key_str, current_version)
Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s", machine_name, checked_version, current_version)
# The first time we want to store the current version, the notification will not be shown,
@ -62,28 +107,11 @@ class FirmwareUpdateCheckerJob(Job):
# notify the user when no new firmware version is available.
if (checked_version != "") and (checked_version != current_version):
Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE")
message = Message(i18n_catalog.i18nc(
"@info Don't translate {machine_name}, since it gets replaced by a printer name!",
"New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format(
machine_name=machine_name),
title=i18n_catalog.i18nc(
"@info:title The %s gets replaced with the printer name.",
"New %s firmware available") % machine_name)
message.addAction("download",
i18n_catalog.i18nc("@action:button", "How to update"),
"[no_icon]",
"[no_description]",
button_style=Message.ActionButtonStyle.LINK,
button_align=Message.ActionButtonStyle.BUTTON_ALIGN_LEFT)
# If we do this in a cool way, the download url should be available in the JSON file
if self._set_download_url_callback:
self._set_download_url_callback("https://ultimaker.com/en/resources/20500-upgrade-firmware")
message = FirmwareUpdateCheckerMessage(machine_id, machine_name, self._lookups.getRedirectUserUrl())
message.actionTriggered.connect(self._callback)
message.show()
else:
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(machine_name))
except Exception as e:
Logger.log("w", "Failed to check for new version: %s", e)

View file

@ -0,0 +1,35 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import List, Optional
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getSettingsKeyForMachine(machine_id: int) -> str:
return "info/latest_checked_firmware_for_{0}".format(machine_id)
class FirmwareUpdateCheckerLookup:
def __init__(self, machine_name, machine_json) -> None:
# Parse all the needed lookup-tables from the ".json" file(s) in the resources folder.
self._machine_id = machine_json.get("id")
self._machine_name = machine_name.lower() # Lower in-case upper-case chars are added to the original json.
self._check_urls = [] # type:List[str]
for check_url in machine_json.get("check_urls"):
self._check_urls.append(check_url)
self._redirect_user = machine_json.get("update_url")
def getMachineId(self) -> Optional[int]:
return self._machine_id
def getMachineName(self) -> Optional[int]:
return self._machine_name
def getCheckUrls(self) -> Optional[List[str]]:
return self._check_urls
def getRedirectUserUrl(self) -> Optional[str]:
return self._redirect_user

View file

@ -0,0 +1,37 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.i18n import i18nCatalog
from UM.Message import Message
i18n_catalog = i18nCatalog("cura")
# Make a separate class, since we need an extra field: The machine-id that this message is about.
class FirmwareUpdateCheckerMessage(Message):
STR_ACTION_DOWNLOAD = "download"
def __init__(self, machine_id: int, machine_name: str, download_url: str) -> None:
super().__init__(i18n_catalog.i18nc(
"@info Don't translate {machine_name}, since it gets replaced by a printer name!",
"New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format(
machine_name = machine_name),
title = i18n_catalog.i18nc(
"@info:title The %s gets replaced with the printer name.",
"New %s firmware available") % machine_name)
self._machine_id = machine_id
self._download_url = download_url
self.addAction(self.STR_ACTION_DOWNLOAD,
i18n_catalog.i18nc("@action:button", "How to update"),
"[no_icon]",
"[no_description]",
button_style = Message.ActionButtonStyle.LINK,
button_align = Message.ActionButtonStyle.BUTTON_ALIGN_LEFT)
def getMachineId(self) -> int:
return self._machine_id
def getDownloadUrl(self) -> str:
return self._download_url

View file

@ -1,12 +1,8 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.i18n import i18nCatalog
from . import FirmwareUpdateChecker
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {}

View file

@ -0,0 +1,69 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.CuraApplication import CuraApplication
from UM.Settings.DefinitionContainer import DefinitionContainer
from cura.MachineAction import MachineAction
from UM.i18n import i18nCatalog
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdateState
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject
from typing import Optional
MYPY = False
if MYPY:
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdater
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from UM.Settings.ContainerInterface import ContainerInterface
catalog = i18nCatalog("cura")
## Upgrade the firmware of a machine by USB with this action.
class FirmwareUpdaterMachineAction(MachineAction):
def __init__(self) -> None:
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Update Firmware"))
self._qml_url = "FirmwareUpdaterMachineAction.qml"
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
self._active_output_device = None # type: Optional[PrinterOutputDevice]
self._active_firmware_updater = None # type: Optional[FirmwareUpdater]
CuraApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
def _onEngineCreated(self) -> None:
CuraApplication.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
def _onContainerAdded(self, container: "ContainerInterface") -> None:
# Add this action as a supported action to all machine definitions if they support USB connection
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine" and container.getMetaDataEntry("supports_usb_connection"):
CuraApplication.getInstance().getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
def _onOutputDevicesChanged(self) -> None:
if self._active_output_device and self._active_output_device.activePrinter:
self._active_output_device.activePrinter.getController().canUpdateFirmwareChanged.disconnect(self._onControllerCanUpdateFirmwareChanged)
output_devices = CuraApplication.getInstance().getMachineManager().printerOutputDevices
self._active_output_device = output_devices[0] if output_devices else None
if self._active_output_device and self._active_output_device.activePrinter:
self._active_output_device.activePrinter.getController().canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
self.outputDeviceCanUpdateFirmwareChanged.emit()
def _onControllerCanUpdateFirmwareChanged(self) -> None:
self.outputDeviceCanUpdateFirmwareChanged.emit()
outputDeviceCanUpdateFirmwareChanged = pyqtSignal()
@pyqtProperty(QObject, notify = outputDeviceCanUpdateFirmwareChanged)
def firmwareUpdater(self) -> Optional["FirmwareUpdater"]:
if self._active_output_device and self._active_output_device.activePrinter.getController().can_update_firmware:
self._active_firmware_updater = self._active_output_device.getFirmwareUpdater()
return self._active_firmware_updater
elif self._active_firmware_updater and self._active_firmware_updater.firmwareUpdateState not in [FirmwareUpdateState.idle, FirmwareUpdateState.completed]:
# During a firmware update, the PrinterOutputDevice is disconnected but the FirmwareUpdater is still there
return self._active_firmware_updater
self._active_firmware_updater = None
return None

View file

@ -0,0 +1,191 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import QtQuick.Dialogs 1.2 // For filedialog
import UM 1.2 as UM
import Cura 1.0 as Cura
Cura.MachineAction
{
anchors.fill: parent;
property bool printerConnected: Cura.MachineManager.printerConnected
property var activeOutputDevice: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null
property bool canUpdateFirmware: activeOutputDevice ? activeOutputDevice.activePrinter.canUpdateFirmware : false
Column
{
id: firmwareUpdaterMachineAction
anchors.fill: parent;
UM.I18nCatalog { id: catalog; name:"cura"}
spacing: UM.Theme.getSize("default_margin").height
Label
{
width: parent.width
text: catalog.i18nc("@title", "Update Firmware")
wrapMode: Text.WordWrap
font.pointSize: 18
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Firmware is the piece of software running directly on your 3D printer. This firmware controls the step motors, regulates the temperature and ultimately makes your printer work.")
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "The firmware shipping with new printers works, but new versions tend to have more features and improvements.");
}
Row
{
anchors.horizontalCenter: parent.horizontalCenter
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
property string firmwareName: Cura.MachineManager.activeMachine.getDefaultFirmwareName()
Button
{
id: autoUpgradeButton
text: catalog.i18nc("@action:button", "Automatically upgrade Firmware");
enabled: parent.firmwareName != "" && canUpdateFirmware
onClicked:
{
updateProgressDialog.visible = true;
activeOutputDevice.updateFirmware(parent.firmwareName);
}
}
Button
{
id: manualUpgradeButton
text: catalog.i18nc("@action:button", "Upload custom Firmware");
enabled: canUpdateFirmware
onClicked:
{
customFirmwareDialog.open()
}
}
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
visible: !printerConnected && !updateProgressDialog.visible
text: catalog.i18nc("@label", "Firmware can not be updated because there is no connection with the printer.");
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
visible: printerConnected && !canUpdateFirmware
text: catalog.i18nc("@label", "Firmware can not be updated because the connection with the printer does not support upgrading firmware.");
}
}
FileDialog
{
id: customFirmwareDialog
title: catalog.i18nc("@title:window", "Select custom firmware")
nameFilters: "Firmware image files (*.hex)"
selectExisting: true
onAccepted:
{
updateProgressDialog.visible = true;
activeOutputDevice.updateFirmware(fileUrl);
}
}
UM.Dialog
{
id: updateProgressDialog
width: minimumWidth
minimumWidth: 500 * screenScaleFactor
height: minimumHeight
minimumHeight: 100 * screenScaleFactor
modality: Qt.ApplicationModal
title: catalog.i18nc("@title:window","Firmware Update")
Column
{
anchors.fill: parent
Label
{
anchors
{
left: parent.left
right: parent.right
}
text: {
if(manager.firmwareUpdater == null)
{
return "";
}
switch (manager.firmwareUpdater.firmwareUpdateState)
{
case 0:
return ""; //Not doing anything (eg; idling)
case 1:
return catalog.i18nc("@label","Updating firmware.");
case 2:
return catalog.i18nc("@label","Firmware update completed.");
case 3:
return catalog.i18nc("@label","Firmware update failed due to an unknown error.");
case 4:
return catalog.i18nc("@label","Firmware update failed due to an communication error.");
case 5:
return catalog.i18nc("@label","Firmware update failed due to an input/output error.");
case 6:
return catalog.i18nc("@label","Firmware update failed due to missing firmware.");
}
}
wrapMode: Text.Wrap
}
ProgressBar
{
id: prog
value: (manager.firmwareUpdater != null) ? manager.firmwareUpdater.firmwareProgress : 0
minimumValue: 0
maximumValue: 100
indeterminate:
{
if(manager.firmwareUpdater == null)
{
return false;
}
return manager.firmwareUpdater.firmwareProgress < 1 && manager.firmwareUpdater.firmwareProgress > 0;
}
anchors
{
left: parent.left;
right: parent.right;
}
}
}
rightButtons: [
Button
{
text: catalog.i18nc("@action:button","Close");
enabled: (manager.firmwareUpdater != null) ? manager.firmwareUpdater.firmwareUpdateState != 1 : true;
onClicked: updateProgressDialog.visible = false;
}
]
}
}

View file

@ -0,0 +1,12 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import FirmwareUpdaterMachineAction
def getMetaData():
return {}
def register(app):
return { "machine_action": [
FirmwareUpdaterMachineAction.FirmwareUpdaterMachineAction()
]}

View file

@ -0,0 +1,8 @@
{
"name": "Firmware Updater",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides a machine actions for updating firmware.",
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -4,15 +4,22 @@
import gzip
from UM.Mesh.MeshReader import MeshReader #The class we're extending/implementing.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType #To add the .gcode.gz files to the MIME type database.
from UM.PluginRegistry import PluginRegistry
## A file reader that reads gzipped g-code.
#
# If you're zipping g-code, you might as well use gzip!
class GCodeGzReader(MeshReader):
def __init__(self) -> None:
super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-compressed-gcode-file",
comment = "Cura Compressed GCode File",
suffixes = ["gcode.gz"]
)
)
self._supported_extensions = [".gcode.gz"]
def _read(self, file_name):

View file

@ -44,6 +44,7 @@ class FlavorParser:
self._extruder_offsets = {} # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._current_layer_thickness = 0.2 # default
self._filament_diameter = 2.85 # default
self._previous_extrusion_value = 0.0 # keep track of the filament retractions
CuraApplication.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)
@ -182,6 +183,7 @@ class FlavorParser:
new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
if new_extrusion_value > e[self._extruder_number]:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
self._previous_extrusion_value = new_extrusion_value
else:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
e[self._extruder_number] = new_extrusion_value
@ -191,6 +193,8 @@ class FlavorParser:
if z > self._previous_z and (z - self._previous_z < 1.5):
self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
self._previous_z = z
elif self._previous_extrusion_value > e[self._extruder_number]:
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType])
else:
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
return self._position(x, y, z, f, e)
@ -227,6 +231,9 @@ class FlavorParser:
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
position.e[self._extruder_number] = params.e
self._previous_extrusion_value = params.e
else:
self._previous_extrusion_value = 0.0
return self._position(
params.x if params.x is not None else position.x,
params.y if params.y is not None else position.y,
@ -286,7 +293,7 @@ class FlavorParser:
self._cancelled = False
# We obtain the filament diameter from the selected extruder to calculate line widths
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
if not global_stack:
return None
@ -329,6 +336,7 @@ class FlavorParser:
min_layer_number = 0
negative_layers = 0
previous_layer = 0
self._previous_extrusion_value = 0.0
for line in stream.split("\n"):
if self._cancelled:

View file

@ -1,4 +1,5 @@
# Copyright (c) 2017 Aleph Objects, Inc.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.FileHandler.FileReader import FileReader
@ -11,13 +12,7 @@ catalog = i18nCatalog("cura")
from . import MarlinFlavorParser, RepRapFlavorParser
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-gcode-file",
comment = "Cura GCode File",
suffixes = ["gcode", "gcode.gz"]
)
)
# Class for loading and parsing G-code files
@ -29,7 +24,15 @@ class GCodeReader(MeshReader):
def __init__(self) -> None:
super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-gcode-file",
comment = "Cura GCode File",
suffixes = ["gcode"]
)
)
self._supported_extensions = [".gcode", ".g"]
self._flavor_reader = None
Application.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)

View file

@ -1,9 +1,9 @@
# Copyright (c) 2017 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import FlavorParser
# This parser is intented for interpret the RepRap Firmware flavor
## This parser is intended to interpret the RepRap Firmware g-code flavor.
class RepRapFlavorParser(FlavorParser.FlavorParser):
def __init__(self):

View file

@ -70,7 +70,7 @@ class GCodeWriter(MeshWriter):
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
scene = Application.getInstance().getController().getScene()
if not hasattr(scene, "gcode_dict"):
self.setInformation(catalog.i18nc("@warning:status", "Please generate G-code before saving."))
self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
return False
gcode_dict = getattr(scene, "gcode_dict")
gcode_list = gcode_dict.get(active_build_plate, None)
@ -86,7 +86,7 @@ class GCodeWriter(MeshWriter):
stream.write(settings)
return True
self.setInformation(catalog.i18nc("@warning:status", "Please generate G-code before saving."))
self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
return False
## Create a new container with container 2 as base and container 1 written over it.

View file

@ -35,7 +35,7 @@ UM.Dialog
width: parent.width
Label {
text: catalog.i18nc("@action:label","Height (mm)")
text: catalog.i18nc("@action:label", "Height (mm)")
width: 150 * screenScaleFactor
anchors.verticalCenter: parent.verticalCenter
}
@ -58,7 +58,7 @@ UM.Dialog
width: parent.width
Label {
text: catalog.i18nc("@action:label","Base (mm)")
text: catalog.i18nc("@action:label", "Base (mm)")
width: 150 * screenScaleFactor
anchors.verticalCenter: parent.verticalCenter
}
@ -81,7 +81,7 @@ UM.Dialog
width: parent.width
Label {
text: catalog.i18nc("@action:label","Width (mm)")
text: catalog.i18nc("@action:label", "Width (mm)")
width: 150 * screenScaleFactor
anchors.verticalCenter: parent.verticalCenter
}
@ -105,7 +105,7 @@ UM.Dialog
width: parent.width
Label {
text: catalog.i18nc("@action:label","Depth (mm)")
text: catalog.i18nc("@action:label", "Depth (mm)")
width: 150 * screenScaleFactor
anchors.verticalCenter: parent.verticalCenter
}
@ -151,7 +151,7 @@ UM.Dialog
width: parent.width
Label {
text: catalog.i18nc("@action:label","Smoothing")
text: catalog.i18nc("@action:label", "Smoothing")
width: 150 * screenScaleFactor
anchors.verticalCenter: parent.verticalCenter
}

View file

@ -152,7 +152,7 @@ class LegacyProfileReader(ProfileReader):
profile.setDirty(True)
#Serialise and deserialise in order to perform the version upgrade.
parser = configparser.ConfigParser(interpolation=None)
parser = configparser.ConfigParser(interpolation = None)
data = profile.serialize()
parser.read_string(data)
parser["general"]["version"] = "1"

View file

@ -405,7 +405,15 @@ Cura.MachineAction
{
if (settingsTabs.currentIndex > 0)
{
manager.updateMaterialForDiameter(settingsTabs.currentIndex - 1);
manager.updateMaterialForDiameter(settingsTabs.currentIndex - 1)
}
}
function setValueFunction(value)
{
if (settingsTabs.currentIndex > 0)
{
const extruderIndex = index.toString()
Cura.MachineManager.activeMachine.extruders[extruderIndex].compatibleMaterialDiameter = value
}
}
property bool isExtruderSetting: true
@ -435,6 +443,18 @@ Cura.MachineAction
property bool allowNegative: true
}
Loader
{
id: extruderCoolingFanNumberField
sourceComponent: numericTextFieldWithUnit
property string settingKey: "machine_extruder_cooling_fan_number"
property string label: catalog.i18nc("@label", "Cooling Fan Number")
property string unit: catalog.i18nc("@label", "")
property bool isExtruderSetting: true
property bool forceUpdateOnChange: true
property bool allowNegative: false
}
Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height }
Row
@ -552,6 +572,7 @@ Cura.MachineAction
property bool _forceUpdateOnChange: (typeof(forceUpdateOnChange) === 'undefined') ? false : forceUpdateOnChange
property string _label: (typeof(label) === 'undefined') ? "" : label
property string _tooltip: (typeof(tooltip) === 'undefined') ? propertyProvider.properties.description : tooltip
property var _setValueFunction: (typeof(setValueFunction) === 'undefined') ? undefined : setValueFunction
UM.SettingPropertyProvider
{
@ -604,14 +625,32 @@ Cura.MachineAction
{
if (propertyProvider && text != propertyProvider.properties.value)
{
propertyProvider.setPropertyValue("value", text);
// For some properties like the extruder-compatible material diameter, they need to
// trigger many updates, such as the available materials, the current material may
// need to be switched, etc. Although setting the diameter can be done directly via
// the provider, all the updates that need to be triggered then need to depend on
// the metadata update, a signal that can be fired way too often. The update functions
// can have if-checks to filter out the irrelevant updates, but still it incurs unnecessary
// overhead.
// The ExtruderStack class has a dedicated function for this call "setCompatibleMaterialDiameter()",
// and it triggers the diameter update signals only when it is needed. Here it is optionally
// choose to use setCompatibleMaterialDiameter() or other more specific functions that
// are available.
if (_setValueFunction !== undefined)
{
_setValueFunction(text)
}
else
{
propertyProvider.setPropertyValue("value", text)
}
if(_forceUpdateOnChange)
{
manager.forceUpdate();
manager.forceUpdate()
}
if(_afterOnEditingFinished)
{
_afterOnEditingFinished();
_afterOnEditingFinished()
}
}
}

View file

@ -3,8 +3,6 @@
from . import MachineSettingsAction
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData():
return {}

View file

@ -1,11 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V.
# This example is released under the terms of the AGPLv3 or higher.
# Cura is released under the terms of the LGPLv3 or higher.
from . import ModelChecker
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData():
return {}

View file

@ -3,6 +3,7 @@
from . import MonitorStage
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")

View file

@ -407,14 +407,9 @@ Item {
function updateFilter()
{
var new_filter = {};
if (printSequencePropertyProvider.properties.value == "one_at_a_time")
{
new_filter["settable_per_meshgroup"] = true;
}
else
{
new_filter["settable_per_mesh"] = true;
}
new_filter["settable_per_mesh"] = true;
// Don't filter on "settable_per_meshgroup" any more when `printSequencePropertyProvider.properties.value`
// is set to "one_at_a_time", because the current backend architecture isn't ready for that.
if(filterInput.text != "")
{

View file

@ -2,6 +2,7 @@
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from typing import Dict, Type, TYPE_CHECKING, List, Optional, cast
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
@ -9,55 +10,62 @@ from UM.Application import Application
from UM.Extension import Extension
from UM.Logger import Logger
import configparser #The script lists are stored in metadata as serialised config files.
import io #To allow configparser to write to a string.
import configparser # The script lists are stored in metadata as serialised config files.
import io # To allow configparser to write to a string.
import os.path
import pkgutil
import sys
import importlib.util
from UM.i18n import i18nCatalog
from cura.CuraApplication import CuraApplication
i18n_catalog = i18nCatalog("cura")
if TYPE_CHECKING:
from .Script import Script
## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated
# g-code files.
class PostProcessingPlugin(QObject, Extension):
def __init__(self, parent = None):
super().__init__(parent)
def __init__(self, parent = None) -> None:
QObject.__init__(self, parent)
Extension.__init__(self)
self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup)
self._view = None
# Loaded scripts are all scripts that can be used
self._loaded_scripts = {}
self._script_labels = {}
self._loaded_scripts = {} # type: Dict[str, Type[Script]]
self._script_labels = {} # type: Dict[str, str]
# Script list contains instances of scripts in loaded_scripts.
# There can be duplicates, which will be executed in sequence.
self._script_list = []
self._script_list = [] # type: List[Script]
self._selected_script_index = -1
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) #When the current printer changes, update the list of scripts.
Application.getInstance().mainWindowChanged.connect(self._createView) #When the main window is created, create the view so that we can display the post-processing icon if necessary.
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) # When the current printer changes, update the list of scripts.
CuraApplication.getInstance().mainWindowChanged.connect(self._createView) # When the main window is created, create the view so that we can display the post-processing icon if necessary.
selectedIndexChanged = pyqtSignal()
@pyqtProperty("QVariant", notify = selectedIndexChanged)
def selectedScriptDefinitionId(self):
@pyqtProperty(str, notify = selectedIndexChanged)
def selectedScriptDefinitionId(self) -> Optional[str]:
try:
return self._script_list[self._selected_script_index].getDefinitionId()
except:
return ""
@pyqtProperty("QVariant", notify=selectedIndexChanged)
def selectedScriptStackId(self):
@pyqtProperty(str, notify=selectedIndexChanged)
def selectedScriptStackId(self) -> Optional[str]:
try:
return self._script_list[self._selected_script_index].getStackId()
except:
return ""
## Execute all post-processing scripts on the gcode.
def execute(self, output_device):
def execute(self, output_device) -> None:
scene = Application.getInstance().getController().getScene()
# If the scene does not have a gcode, do nothing
if not hasattr(scene, "gcode_dict"):
@ -67,7 +75,7 @@ class PostProcessingPlugin(QObject, Extension):
return
# get gcode list for the active build plate
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list:
return
@ -86,16 +94,17 @@ class PostProcessingPlugin(QObject, Extension):
Logger.log("e", "Already post processed")
@pyqtSlot(int)
def setSelectedScriptIndex(self, index):
self._selected_script_index = index
self.selectedIndexChanged.emit()
def setSelectedScriptIndex(self, index: int) -> None:
if self._selected_script_index != index:
self._selected_script_index = index
self.selectedIndexChanged.emit()
@pyqtProperty(int, notify = selectedIndexChanged)
def selectedScriptIndex(self):
def selectedScriptIndex(self) -> int:
return self._selected_script_index
@pyqtSlot(int, int)
def moveScript(self, index, new_index):
def moveScript(self, index: int, new_index: int) -> None:
if new_index < 0 or new_index > len(self._script_list) - 1:
return # nothing needs to be done
else:
@ -107,7 +116,7 @@ class PostProcessingPlugin(QObject, Extension):
## Remove a script from the active script list by index.
@pyqtSlot(int)
def removeScriptByIndex(self, index):
def removeScriptByIndex(self, index: int) -> None:
self._script_list.pop(index)
if len(self._script_list) - 1 < self._selected_script_index:
self._selected_script_index = len(self._script_list) - 1
@ -118,14 +127,16 @@ class PostProcessingPlugin(QObject, Extension):
## Load all scripts from all paths where scripts can be found.
#
# This should probably only be done on init.
def loadAllScripts(self):
if self._loaded_scripts: #Already loaded.
def loadAllScripts(self) -> None:
if self._loaded_scripts: # Already loaded.
return
#The PostProcessingPlugin path is for built-in scripts.
#The Resources path is where the user should store custom scripts.
#The Preferences path is legacy, where the user may previously have stored scripts.
# The PostProcessingPlugin path is for built-in scripts.
# The Resources path is where the user should store custom scripts.
# The Preferences path is legacy, where the user may previously have stored scripts.
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Resources), Resources.getStoragePath(Resources.Preferences)]:
if root is None:
continue
path = os.path.join(root, "scripts")
if not os.path.isdir(path):
try:
@ -139,7 +150,7 @@ class PostProcessingPlugin(QObject, Extension):
## Load all scripts from provided path.
# This should probably only be done on init.
# \param path Path to check for scripts.
def loadScripts(self, path):
def loadScripts(self, path: str) -> None:
## Load all scripts in the scripts folders
scripts = pkgutil.iter_modules(path = [path])
for loader, script_name, ispkg in scripts:
@ -148,6 +159,8 @@ class PostProcessingPlugin(QObject, Extension):
try:
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py"))
loaded_script = importlib.util.module_from_spec(spec)
if spec.loader is None:
continue
spec.loader.exec_module(loaded_script)
sys.modules[script_name] = loaded_script #TODO: This could be a security risk. Overwrite any module with a user-provided name?
@ -172,23 +185,24 @@ class PostProcessingPlugin(QObject, Extension):
loadedScriptListChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = loadedScriptListChanged)
def loadedScriptList(self):
def loadedScriptList(self) -> List[str]:
return sorted(list(self._loaded_scripts.keys()))
@pyqtSlot(str, result = str)
def getScriptLabelByKey(self, key):
return self._script_labels[key]
def getScriptLabelByKey(self, key: str) -> Optional[str]:
return self._script_labels.get(key)
scriptListChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = scriptListChanged)
def scriptList(self):
@pyqtProperty("QStringList", notify = scriptListChanged)
def scriptList(self) -> List[str]:
script_list = [script.getSettingData()["key"] for script in self._script_list]
return script_list
@pyqtSlot(str)
def addScriptToList(self, key):
def addScriptToList(self, key: str) -> None:
Logger.log("d", "Adding script %s to list.", key)
new_script = self._loaded_scripts[key]()
new_script.initialize()
self._script_list.append(new_script)
self.setSelectedScriptIndex(len(self._script_list) - 1)
self.scriptListChanged.emit()
@ -196,81 +210,89 @@ class PostProcessingPlugin(QObject, Extension):
## When the global container stack is changed, swap out the list of active
# scripts.
def _onGlobalContainerStackChanged(self):
def _onGlobalContainerStackChanged(self) -> None:
self.loadAllScripts()
new_stack = Application.getInstance().getGlobalContainerStack()
if new_stack is None:
return
self._script_list.clear()
if not new_stack.getMetaDataEntry("post_processing_scripts"): #Missing or empty.
self.scriptListChanged.emit() #Even emit this if it didn't change. We want it to write the empty list to the stack's metadata.
if not new_stack.getMetaDataEntry("post_processing_scripts"): # Missing or empty.
self.scriptListChanged.emit() # Even emit this if it didn't change. We want it to write the empty list to the stack's metadata.
return
self._script_list.clear()
scripts_list_strs = new_stack.getMetaDataEntry("post_processing_scripts")
for script_str in scripts_list_strs.split("\n"): #Encoded config files should never contain three newlines in a row. At most 2, just before section headers.
if not script_str: #There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here).
for script_str in scripts_list_strs.split("\n"): # Encoded config files should never contain three newlines in a row. At most 2, just before section headers.
if not script_str: # There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here).
continue
script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") #Unescape escape sequences.
script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") # Unescape escape sequences.
script_parser = configparser.ConfigParser(interpolation = None)
script_parser.optionxform = str #Don't transform the setting keys as they are case-sensitive.
script_parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive.
script_parser.read_string(script_str)
for script_name, settings in script_parser.items(): #There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script.
if script_name == "DEFAULT": #ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one.
for script_name, settings in script_parser.items(): # There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script.
if script_name == "DEFAULT": # ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one.
continue
if script_name not in self._loaded_scripts: #Don't know this post-processing plug-in.
if script_name not in self._loaded_scripts: # Don't know this post-processing plug-in.
Logger.log("e", "Unknown post-processing script {script_name} was encountered in this global stack.".format(script_name = script_name))
continue
new_script = self._loaded_scripts[script_name]()
for setting_key, setting_value in settings.items(): #Put all setting values into the script.
new_script._instance.setProperty(setting_key, "value", setting_value)
new_script.initialize()
for setting_key, setting_value in settings.items(): # Put all setting values into the script.
if new_script._instance is not None:
new_script._instance.setProperty(setting_key, "value", setting_value)
self._script_list.append(new_script)
self.setSelectedScriptIndex(0)
self.scriptListChanged.emit()
@pyqtSlot()
def writeScriptsToStack(self):
script_list_strs = []
def writeScriptsToStack(self) -> None:
script_list_strs = [] # type: List[str]
for script in self._script_list:
parser = configparser.ConfigParser(interpolation = None) #We'll encode the script as a config with one section. The section header is the key and its values are the settings.
parser.optionxform = str #Don't transform the setting keys as they are case-sensitive.
parser = configparser.ConfigParser(interpolation = None) # We'll encode the script as a config with one section. The section header is the key and its values are the settings.
parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive.
script_name = script.getSettingData()["key"]
parser.add_section(script_name)
for key in script.getSettingData()["settings"]:
value = script.getSettingValueByKey(key)
parser[script_name][key] = str(value)
serialized = io.StringIO() #ConfigParser can only write to streams. Fine.
serialized = io.StringIO() # ConfigParser can only write to streams. Fine.
parser.write(serialized)
serialized.seek(0)
script_str = serialized.read()
script_str = script_str.replace("\\\\", r"\\\\").replace("\n", r"\\\n") #Escape newlines because configparser sees those as section delimiters.
script_str = script_str.replace("\\\\", r"\\\\").replace("\n", r"\\\n") # Escape newlines because configparser sees those as section delimiters.
script_list_strs.append(script_str)
script_list_strs = "\n".join(script_list_strs) #ConfigParser should never output three newlines in a row when serialised, so it's a safe delimiter.
script_list_string = "\n".join(script_list_strs) # ConfigParser should never output three newlines in a row when serialised, so it's a safe delimiter.
global_stack = Application.getInstance().getGlobalContainerStack()
if global_stack is None:
return
if "post_processing_scripts" not in global_stack.getMetaData():
global_stack.setMetaDataEntry("post_processing_scripts", "")
Application.getInstance().getGlobalContainerStack().setMetaDataEntry("post_processing_scripts", script_list_strs)
global_stack.setMetaDataEntry("post_processing_scripts", script_list_string)
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
def _createView(self):
def _createView(self) -> None:
Logger.log("d", "Creating post processing plugin view.")
self.loadAllScripts()
# Create the plugin dialog component
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
path = os.path.join(cast(str, PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin")), "PostProcessingPlugin.qml")
self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
if self._view is None:
Logger.log("e", "Not creating PostProcessing button near save button because the QML component failed to be created.")
return
Logger.log("d", "Post processing view created.")
# Create the save button component
Application.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
CuraApplication.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
## Show the (GUI) popup of the post processing plugin.
def showPopup(self):
def showPopup(self) -> None:
if self._view is None:
self._createView()
if self._view is None:
@ -282,8 +304,9 @@ class PostProcessingPlugin(QObject, Extension):
# To do this we use the global container stack propertyChanged.
# Re-slicing is necessary for setting changes in this plugin, because the changes
# are applied only once per "fresh" gcode
def _propertyChanged(self):
def _propertyChanged(self) -> None:
global_container_stack = Application.getInstance().getGlobalContainerStack()
global_container_stack.propertyChanged.emit("post_processing_plugin", "value")
if global_container_stack is not None:
global_container_stack.propertyChanged.emit("post_processing_plugin", "value")

View file

@ -62,6 +62,7 @@ UM.Dialog
anchors.right: parent.right
anchors.rightMargin: base.textMargin
font: UM.Theme.getFont("large")
elide: Text.ElideRight
}
ListView
{
@ -115,6 +116,7 @@ UM.Dialog
{
wrapMode: Text.Wrap
text: control.text
elide: Text.ElideRight
color: activeScriptButton.checked ? palette.highlightedText : palette.text
}
}
@ -275,6 +277,7 @@ UM.Dialog
anchors.leftMargin: base.textMargin
anchors.right: parent.right
anchors.rightMargin: base.textMargin
elide: Text.ElideRight
height: 20 * screenScaleFactor
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")

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