Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/Ultimaker/Cura.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'cura/CuraApplication.py')
-rwxr-xr-xcura/CuraApplication.py398
1 files changed, 244 insertions, 154 deletions
diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py
index 7b17583f68..f0c69d5a61 100755
--- a/cura/CuraApplication.py
+++ b/cura/CuraApplication.py
@@ -4,82 +4,68 @@
import os
import sys
import time
-from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any
+from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any, Dict
import numpy
-
from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
from PyQt5.QtGui import QColor, QIcon
-from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
+from PyQt5.QtWidgets import QMessageBox
-from UM.i18n import i18nCatalog
+import UM.Util
+import cura.Settings.cura_empty_instance_containers
from UM.Application import Application
from UM.Decorators import override
from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger
-from UM.Message import Message
-from UM.Platform import Platform
-from UM.PluginError import PluginNotFoundError
-from UM.Resources import Resources
-from UM.Preferences import Preferences
-from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
-import UM.Util
-from UM.View.SelectionPass import SelectionPass # For typing.
-
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Matrix import Matrix
from UM.Math.Quaternion import Quaternion
from UM.Math.Vector import Vector
-
from UM.Mesh.ReadMeshJob import ReadMeshJob
-
+from UM.Message import Message
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.SetTransformOperation import SetTransformOperation
-
+from UM.Platform import Platform
+from UM.PluginError import PluginNotFoundError
+from UM.Preferences import Preferences
+from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
+from UM.Resources import Resources
from UM.Scene.Camera import Camera
from UM.Scene.GroupDecorator import GroupDecorator
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
+from UM.Scene.SceneNodeSettings import SceneNodeSettings
from UM.Scene.Selection import Selection
from UM.Scene.ToolHandle import ToolHandle
-
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.Validator import Validator
-
+from UM.View.SelectionPass import SelectionPass # For typing.
from UM.Workspace.WorkspaceReader import WorkspaceReader
-
+from UM.i18n import i18nCatalog
+from cura import ApplicationMetadata
from cura.API import CuraAPI
-
+from cura.API.Account import Account
from cura.Arranging.Arrange import Arrange
-from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
-from cura.Arranging.ShapeArray import ShapeArray
-
-from cura.Operations.SetParentOperation import SetParentOperation
-
-from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
-from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
-from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
-from cura.Scene.CuraSceneController import CuraSceneController
-from cura.Scene.CuraSceneNode import CuraSceneNode
-
-from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
-from cura.Scene import ZOffsetDecorator
+from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
+from cura.Arranging.Nest2DArrange import arrange
from cura.Machines.MachineErrorChecker import MachineErrorChecker
-
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel
+from cura.Machines.Models.DiscoveredCloudPrintersModel import DiscoveredCloudPrintersModel
from cura.Machines.Models.ExtrudersModel import ExtrudersModel
from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel
+from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel
+from cura.Machines.Models.IntentModel import IntentModel
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
@@ -89,51 +75,47 @@ from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfile
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
from cura.Machines.Models.UserChangesModel import UserChangesModel
-from cura.Machines.Models.IntentModel import IntentModel
-from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel
-
-from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
+from cura.Operations.SetParentOperation import SetParentOperation
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
-
-import cura.Settings.cura_empty_instance_containers
+from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
+from cura.Scene import ZOffsetDecorator
+from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
+from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
+from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
+from cura.Scene.CuraSceneController import CuraSceneController
+from cura.Scene.CuraSceneNode import CuraSceneNode
+from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack
+from cura.Settings.GlobalStack import GlobalStack
+from cura.Settings.IntentManager import IntentManager
from cura.Settings.MachineManager import MachineManager
from cura.Settings.MachineNameValidator import MachineNameValidator
-from cura.Settings.IntentManager import IntentManager
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
-
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
-
from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation
+from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
from cura.UI.MachineSettingsManager import MachineSettingsManager
from cura.UI.ObjectsModel import ObjectsModel
-from cura.UI.TextManager import TextManager
-from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
from cura.UI.RecommendedMode import RecommendedMode
+from cura.UI.TextManager import TextManager
from cura.UI.WelcomePagesModel import WelcomePagesModel
from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel
-
+from cura.UltimakerCloud import UltimakerCloudConstants
from cura.Utils.NetworkingUtil import NetworkingUtil
-
-from .SingleInstance import SingleInstance
-from .AutoSave import AutoSave
-from . import PlatformPhysics
from . import BuildVolume
from . import CameraAnimation
from . import CuraActions
+from . import PlatformPhysics
from . import PrintJobPreviewImageProvider
-
-from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
-
-from cura import ApplicationMetadata, UltimakerCloudAuthentication
-from cura.Settings.GlobalStack import GlobalStack
+from .AutoSave import AutoSave
+from .SingleInstance import SingleInstance
if TYPE_CHECKING:
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
@@ -145,7 +127,7 @@ class CuraApplication(QtApplication):
# SettingVersion represents the set of settings available in the machine/extruder definitions.
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
# changes of the settings.
- SettingVersion = 11
+ SettingVersion = 16
Created = False
@@ -222,9 +204,11 @@ class CuraApplication(QtApplication):
self._quality_management_model = None
self._discovered_printer_model = DiscoveredPrintersModel(self, parent = self)
+ self._discovered_cloud_printers_model = DiscoveredCloudPrintersModel(self, parent = self)
self._first_start_machine_actions_model = None
self._welcome_pages_model = WelcomePagesModel(self, parent = self)
self._add_printer_pages_model = AddPrinterPagesModel(self, parent = self)
+ self._add_printer_pages_model_without_cancel = AddPrinterPagesModel(self, parent = self)
self._whats_new_pages_model = WhatsNewPagesModel(self, parent = self)
self._text_manager = TextManager(parent = self)
@@ -263,6 +247,7 @@ class CuraApplication(QtApplication):
# Backups
self._auto_save = None # type: Optional[AutoSave]
+ self._enable_save = True
self._container_registry_class = CuraContainerRegistry
# Redefined here in order to please the typing.
@@ -272,15 +257,22 @@ class CuraApplication(QtApplication):
@pyqtProperty(str, constant=True)
def ultimakerCloudApiRootUrl(self) -> str:
- return UltimakerCloudAuthentication.CuraCloudAPIRoot
+ return UltimakerCloudConstants.CuraCloudAPIRoot
@pyqtProperty(str, constant = True)
def ultimakerCloudAccountRootUrl(self) -> str:
- return UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
+ return UltimakerCloudConstants.CuraCloudAccountAPIRoot
+
+ @pyqtProperty(str, constant=True)
+ def ultimakerDigitalFactoryUrl(self) -> str:
+ return UltimakerCloudConstants.CuraDigitalFactoryURL
- # Adds command line options to the command line parser. This should be called after the application is created and
- # before the pre-start.
def addCommandLineOptions(self):
+ """Adds command line options to the command line parser.
+
+ This should be called after the application is created and before the pre-start.
+ """
+
super().addCommandLineOptions()
self._cli_parser.add_argument("--help", "-h",
action = "store_true",
@@ -322,6 +314,9 @@ class CuraApplication(QtApplication):
super().initialize()
+ self._preferences.addPreference("cura/single_instance", False)
+ self._use_single_instance = self._preferences.getValue("cura/single_instance")
+
self.__sendCommandToSingleInstance()
self._initializeSettingDefinitions()
self._initializeSettingFunctions()
@@ -342,8 +337,9 @@ class CuraApplication(QtApplication):
Logger.log("i", "Single instance commands were sent, exiting")
sys.exit(0)
- # Adds expected directory names and search paths for Resources.
def __addExpectedResourceDirsAndSearchPaths(self):
+ """Adds expected directory names and search paths for Resources."""
+
# this list of dir names will be used by UM to detect an old cura directory
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]:
Resources.addExpectedDirNameInData(dir_name)
@@ -385,9 +381,12 @@ class CuraApplication(QtApplication):
SettingDefinition.addSettingType("[int]", None, str, None)
- # Adds custom property types, settings types, and extra operators (functions) that need to be registered in
- # SettingDefinition and SettingFunction.
def _initializeSettingFunctions(self):
+ """Adds custom property types, settings types, and extra operators (functions).
+
+ Whom need to be registered in SettingDefinition and SettingFunction.
+ """
+
self._cura_formula_functions = CuraFormulaFunctions(self)
SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
@@ -397,8 +396,9 @@ class CuraApplication(QtApplication):
SettingFunction.registerOperator("valueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndex)
SettingFunction.registerOperator("extruderValueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndexInExtruder)
- # Adds all resources and container related resources.
def __addAllResourcesAndContainerResources(self) -> None:
+ """Adds all resources and container related resources."""
+
Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
Resources.addStorageType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes")
Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
@@ -423,8 +423,9 @@ class CuraApplication(QtApplication):
Resources.addType(self.ResourceTypes.QmlFiles, "qml")
Resources.addType(self.ResourceTypes.Firmware, "firmware")
- # Adds all empty containers.
def __addAllEmptyContainers(self) -> None:
+ """Adds all empty containers."""
+
# Add empty variant, material and quality containers.
# Since they are empty, they should never be serialized and instead just programmatically created.
# We need them to simplify the switching between materials.
@@ -449,9 +450,10 @@ class CuraApplication(QtApplication):
self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_changes_container)
self.empty_quality_changes_container = cura.Settings.cura_empty_instance_containers.empty_quality_changes_container
- # Initializes the version upgrade manager with by providing the paths for each resource type and the latest
- # versions.
def __setLatestResouceVersionsForVersionUpgrade(self):
+ """Initializes the version upgrade manager with by providing the paths for each resource type and the latest
+ versions. """
+
self._version_upgrade_manager.setCurrentVersions(
{
("quality", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
@@ -466,12 +468,16 @@ class CuraApplication(QtApplication):
}
)
- # Runs preparations that needs to be done before the starting process.
def startSplashWindowPhase(self) -> None:
+ """Runs preparations that needs to be done before the starting process."""
+
super().startSplashWindowPhase()
if not self.getIsHeadLess():
- self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
+ try:
+ self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
+ except FileNotFoundError:
+ Logger.log("w", "Unable to find the window icon.")
self.setRequiredPlugins([
# Misc.:
@@ -523,7 +529,7 @@ class CuraApplication(QtApplication):
# Set the setting version for Preferences
preferences = self.getPreferences()
preferences.addPreference("metadata/setting_version", 0)
- preferences.setValue("metadata/setting_version", self.SettingVersion) #Don't make it equal to the default so that the setting version always gets written to the file.
+ preferences.setValue("metadata/setting_version", self.SettingVersion) # Don't make it equal to the default so that the setting version always gets written to the file.
preferences.addPreference("cura/active_mode", "simple")
@@ -627,12 +633,13 @@ class CuraApplication(QtApplication):
def callConfirmExitDialogCallback(self, yes_or_no: bool) -> None:
self._confirm_exit_dialog_callback(yes_or_no)
- ## Signal to connect preferences action in QML
showPreferencesWindow = pyqtSignal()
+ """Signal to connect preferences action in QML"""
- ## Show the preferences window
@pyqtSlot()
def showPreferences(self) -> None:
+ """Show the preferences window"""
+
self.showPreferencesWindow.emit()
# This is called by drag-and-dropping curapackage files.
@@ -646,14 +653,13 @@ class CuraApplication(QtApplication):
return self._global_container_stack
@override(Application)
- def setGlobalContainerStack(self, stack: "GlobalStack") -> None:
+ def setGlobalContainerStack(self, stack: Optional["GlobalStack"]) -> None:
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing Active Machine..."))
super().setGlobalContainerStack(stack)
- ## A reusable dialogbox
- #
showMessageBox = pyqtSignal(str,str, str, str, int, int,
arguments = ["title", "text", "informativeText", "detailedText","buttons", "icon"])
+ """A reusable dialogbox"""
def messageBox(self, title, text,
informativeText = "",
@@ -706,15 +712,20 @@ class CuraApplication(QtApplication):
self._message_box_callback = None
self._message_box_callback_arguments = []
+ def enableSave(self, enable: bool):
+ self._enable_save = enable
+
# Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
def saveSettings(self) -> None:
- if not self.started:
+ if not self.started or not self._enable_save:
# Do not do saving during application start or when data should not be saved on quit.
return
ContainerRegistry.getInstance().saveDirtyContainers()
self.savePreferences()
def saveStack(self, stack):
+ if not self._enable_save:
+ return
ContainerRegistry.getInstance().saveContainer(stack)
@pyqtSlot(str, result = QUrl)
@@ -726,9 +737,12 @@ class CuraApplication(QtApplication):
def setDefaultPath(self, key, default_path):
self.getPreferences().setValue("local_file/%s" % key, QUrl(default_path).toLocalFile())
- ## Handle loading of all plugin types (and the backend explicitly)
- # \sa PluginRegistry
def _loadPlugins(self) -> None:
+ """Handle loading of all plugin types (and the backend explicitly)
+
+ :py:class:`Uranium.UM.PluginRegistry`
+ """
+
self._plugin_registry.setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion)
self._plugin_registry.addType("profile_reader", self._addProfileReader)
@@ -742,8 +756,7 @@ class CuraApplication(QtApplication):
self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib" + suffix, "cura"))
if not hasattr(sys, "frozen"):
self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
- self._plugin_registry.loadPlugin("ConsoleLogger")
- self._plugin_registry.loadPlugin("CuraEngineBackend")
+ self._plugin_registry.preloaded_plugins.append("ConsoleLogger")
self._plugin_registry.loadPlugins()
@@ -752,9 +765,12 @@ class CuraApplication(QtApplication):
self._plugins_loaded = True
- ## Set a short, user-friendly hint about current loading status.
- # The way this message is displayed depends on application state
def _setLoadingHint(self, hint: str):
+ """Set a short, user-friendly hint about current loading status.
+
+ The way this message is displayed depends on application state
+ """
+
if self.started:
Logger.info(hint)
else:
@@ -786,6 +802,8 @@ class CuraApplication(QtApplication):
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing build volume..."))
root = self.getController().getScene().getRoot()
self._volume = BuildVolume.BuildVolume(self, root)
+
+ # Ensure that the old style arranger still works.
Arrange.build_volume = self._volume
# initialize info objects
@@ -801,6 +819,7 @@ class CuraApplication(QtApplication):
self._output_device_manager.start()
self._welcome_pages_model.initialize()
self._add_printer_pages_model.initialize()
+ self._add_printer_pages_model_without_cancel.initialize(cancellable = False)
self._whats_new_pages_model.initialize()
# Detect in which mode to run and execute that mode
@@ -838,13 +857,16 @@ class CuraApplication(QtApplication):
self.callLater(self._openFile, file_name)
initializationFinished = pyqtSignal()
+ showAddPrintersUncancellableDialog = pyqtSignal() # Used to show the add printers dialog with a greyed background
- ## Run Cura without GUI elements and interaction (server mode).
def runWithoutGUI(self):
+ """Run Cura without GUI elements and interaction (server mode)."""
+
self.closeSplash()
- ## Run Cura with GUI (desktop mode).
def runWithGUI(self):
+ """Run Cura with GUI (desktop mode)."""
+
self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Setting up scene..."))
controller = self.getController()
@@ -873,8 +895,9 @@ class CuraApplication(QtApplication):
# Initialize camera tool
camera_tool = controller.getTool("CameraTool")
- camera_tool.setOrigin(Vector(0, 100, 0))
- camera_tool.setZoomRange(0.1, 2000)
+ if camera_tool:
+ camera_tool.setOrigin(Vector(0, 100, 0))
+ camera_tool.setZoomRange(0.1, 2000)
# Initialize camera animations
self._camera_animation = CameraAnimation.CameraAnimation()
@@ -901,6 +924,10 @@ class CuraApplication(QtApplication):
def getDiscoveredPrintersModel(self, *args) -> "DiscoveredPrintersModel":
return self._discovered_printer_model
+ @pyqtSlot(result=QObject)
+ def getDiscoveredCloudPrintersModel(self, *args) -> "DiscoveredCloudPrintersModel":
+ return self._discovered_cloud_printers_model
+
@pyqtSlot(result = QObject)
def getFirstStartMachineActionsModel(self, *args) -> "FirstStartMachineActionsModel":
if self._first_start_machine_actions_model is None:
@@ -922,6 +949,10 @@ class CuraApplication(QtApplication):
return self._add_printer_pages_model
@pyqtSlot(result = QObject)
+ def getAddPrinterPagesModelWithoutCancel(self, *args) -> "AddPrinterPagesModel":
+ return self._add_printer_pages_model_without_cancel
+
+ @pyqtSlot(result = QObject)
def getWhatsNewPagesModel(self, *args) -> "WhatsNewPagesModel":
return self._whats_new_pages_model
@@ -994,10 +1025,13 @@ class CuraApplication(QtApplication):
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
return self._setting_inheritance_manager
- ## Get the machine action manager
- # We ignore any *args given to this, as we also register the machine manager as qml singleton.
- # It wants to give this function an engine and script engine, but we don't care about that.
def getMachineActionManager(self, *args: Any) -> MachineActionManager.MachineActionManager:
+ """Get the machine action manager
+
+ We ignore any *args given to this, as we also register the machine manager as qml singleton.
+ It wants to give this function an engine and script engine, but we don't care about that.
+ """
+
return cast(MachineActionManager.MachineActionManager, self._machine_action_manager)
@pyqtSlot(result = QObject)
@@ -1017,8 +1051,9 @@ class CuraApplication(QtApplication):
self._simple_mode_settings_manager = SimpleModeSettingsManager()
return self._simple_mode_settings_manager
- ## Handle Qt events
def event(self, event):
+ """Handle Qt events"""
+
if event.type() == QEvent.FileOpen:
if self._plugins_loaded:
self._openFile(event.file())
@@ -1030,8 +1065,9 @@ class CuraApplication(QtApplication):
def getAutoSave(self) -> Optional[AutoSave]:
return self._auto_save
- ## Get print information (duration / material used)
def getPrintInformation(self):
+ """Get print information (duration / material used)"""
+
return self._print_information
def getQualityProfilesDropDownMenuModel(self, *args, **kwargs):
@@ -1047,10 +1083,12 @@ class CuraApplication(QtApplication):
def getCuraAPI(self, *args, **kwargs) -> "CuraAPI":
return self._cura_API
- ## Registers objects for the QML engine to use.
- #
- # \param engine The QML engine.
def registerObjects(self, engine):
+ """Registers objects for the QML engine to use.
+
+ :param engine: The QML engine.
+ """
+
super().registerObjects(engine)
# global contexts
@@ -1099,6 +1137,7 @@ class CuraApplication(QtApplication):
self.processEvents()
qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel")
+ qmlRegisterType(DiscoveredCloudPrintersModel, "Cura", 1, 7, "DiscoveredCloudPrintersModel")
qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
"QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
qmlRegisterSingletonType(CustomQualityProfilesDropDownMenuModel, "Cura", 1, 0,
@@ -1121,6 +1160,7 @@ class CuraApplication(QtApplication):
from cura.API import CuraAPI
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
+ qmlRegisterUncreatableType(Account, "Cura", 1, 0, "AccountSyncState", "Could not create AccountSyncState")
# 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")))
@@ -1184,8 +1224,9 @@ class CuraApplication(QtApplication):
if node is not None and (node.getMeshData() is not None or node.callDecoration("getLayerData")):
self._update_platform_activity_timer.start()
- ## Update scene bounding box for current build plate
def updatePlatformActivity(self, node = None):
+ """Update scene bounding box for current build plate"""
+
count = 0
scene_bounding_box = None
is_block_slicing_node = False
@@ -1229,9 +1270,10 @@ class CuraApplication(QtApplication):
self._platform_activity = True if count > 0 else False
self.activityChanged.emit()
- ## Select all nodes containing mesh data in the scene.
@pyqtSlot()
def selectAll(self):
+ """Select all nodes containing mesh data in the scene."""
+
if not self.getController().getToolsEnabled():
return
@@ -1250,9 +1292,10 @@ class CuraApplication(QtApplication):
Selection.add(node)
- ## Reset all translation on nodes with mesh data.
@pyqtSlot()
def resetAllTranslation(self):
+ """Reset all translation on nodes with mesh data."""
+
Logger.log("i", "Resetting all scene translations")
nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
@@ -1278,9 +1321,10 @@ class CuraApplication(QtApplication):
op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0)))
op.push()
- ## Reset all transformations on nodes with mesh data.
@pyqtSlot()
def resetAll(self):
+ """Reset all transformations on nodes with mesh data."""
+
Logger.log("i", "Resetting all scene transformations")
nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
@@ -1306,9 +1350,10 @@ class CuraApplication(QtApplication):
op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1)))
op.push()
- ## Arrange all objects.
@pyqtSlot()
def arrangeObjectsToAllBuildPlates(self) -> None:
+ """Arrange all objects."""
+
nodes_to_arrange = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not isinstance(node, SceneNode):
@@ -1337,6 +1382,7 @@ class CuraApplication(QtApplication):
def arrangeAll(self) -> None:
nodes_to_arrange = []
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
+ locked_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if not isinstance(node, SceneNode):
continue
@@ -1358,20 +1404,28 @@ class CuraApplication(QtApplication):
# Skip nodes that are too big
bounding_box = node.getBoundingBox()
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
- nodes_to_arrange.append(node)
- self.arrange(nodes_to_arrange, fixed_nodes = [])
+ # Arrange only the unlocked nodes and keep the locked ones in place
+ if UM.Util.parseBool(node.getSetting(SceneNodeSettings.LockPosition)):
+ locked_nodes.append(node)
+ else:
+ nodes_to_arrange.append(node)
+ self.arrange(nodes_to_arrange, locked_nodes)
- ## Arrange a set of nodes given a set of fixed nodes
- # \param nodes nodes that we have to place
- # \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
+ """Arrange a set of nodes given a set of fixed nodes
+
+ :param nodes: nodes that we have to place
+ :param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes
+ """
+
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
job.start()
- ## Reload all mesh data on the screen from file.
@pyqtSlot()
def reloadAll(self) -> None:
+ """Reload all mesh data on the screen from file."""
+
Logger.log("i", "Reloading all loaded mesh data.")
nodes = []
has_merged_nodes = False
@@ -1397,22 +1451,29 @@ class CuraApplication(QtApplication):
if not nodes:
return
+ objects_in_filename = {} # type: Dict[str, List[CuraSceneNode]]
for node in nodes:
mesh_data = node.getMeshData()
-
if mesh_data:
file_name = mesh_data.getFileName()
if file_name:
- job = ReadMeshJob(file_name)
- job._node = node # type: ignore
- job.finished.connect(self._reloadMeshFinished)
- if has_merged_nodes:
- job.finished.connect(self.updateOriginOfMergedMeshes)
-
- job.start()
+ if file_name not in objects_in_filename:
+ objects_in_filename[file_name] = []
+ if file_name in objects_in_filename:
+ objects_in_filename[file_name].append(node)
else:
Logger.log("w", "Unable to reload data because we don't have a filename.")
+ for file_name, nodes in objects_in_filename.items():
+ for node in nodes:
+ job = ReadMeshJob(file_name)
+ job._node = node # type: ignore
+ job.finished.connect(self._reloadMeshFinished)
+ if has_merged_nodes:
+ job.finished.connect(self.updateOriginOfMergedMeshes)
+
+ job.start()
+
@pyqtSlot("QStringList")
def setExpandedCategories(self, categories: List[str]) -> None:
categories = list(set(categories))
@@ -1461,21 +1522,19 @@ class CuraApplication(QtApplication):
# Move each node to the same position.
for mesh, node in zip(meshes, group_node.getChildren()):
- transformation = node.getLocalTransformation()
- transformation.setTranslation(zero_translation)
- transformed_mesh = mesh.getTransformed(transformation)
-
+ node.setTransformation(Matrix())
# Align the object around its zero position
# and also apply the offset to center it inside the group.
- node.setPosition(-transformed_mesh.getZeroPosition() - offset)
+ node.setPosition(-mesh.getZeroPosition() - offset)
# Use the previously found center of the group bounding box as the new location of the group
group_node.setPosition(group_node.getBoundingBox().center)
group_node.setName("MergedMesh") # add a specific name to distinguish this node
- ## Updates origin position of all merged meshes
def updateOriginOfMergedMeshes(self, _):
+ """Updates origin position of all merged meshes"""
+
group_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
@@ -1587,13 +1646,31 @@ class CuraApplication(QtApplication):
fileLoaded = pyqtSignal(str)
fileCompleted = pyqtSignal(str)
- def _reloadMeshFinished(self, job):
- # TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh!
- job_result = job.getResult()
+ def _reloadMeshFinished(self, job) -> None:
+ """
+ Function called whenever a ReadMeshJob finishes in the background. It reloads a specific node object in the
+ scene from its source file. The function gets all the nodes that exist in the file through the job result, and
+ then finds the scene node that it wants to refresh by its object id. Each job refreshes only one node.
+
+ :param job: The :py:class:`Uranium.UM.ReadMeshJob.ReadMeshJob` running in the background that reads all the
+ meshes in a file
+ """
+
+ job_result = job.getResult() # nodes that exist inside the file read by this job
if len(job_result) == 0:
Logger.log("e", "Reloading the mesh failed.")
return
- mesh_data = job_result[0].getMeshData()
+ object_found = False
+ mesh_data = None
+ # Find the node to be refreshed based on its id
+ for job_result_node in job_result:
+ if job_result_node.getId() == job._node.getId():
+ mesh_data = job_result_node.getMeshData()
+ object_found = True
+ break
+ if not object_found:
+ Logger.warning("The object with id {} no longer exists! Keeping the old version in the scene.".format(job_result_node.getId()))
+ return
if not mesh_data:
Logger.log("w", "Could not find a mesh in reloaded node.")
return
@@ -1624,12 +1701,15 @@ class CuraApplication(QtApplication):
def additionalComponents(self):
return self._additional_components
- ## Add a component to a list of components to be reparented to another area in the GUI.
- # The actual reparenting is done by the area itself.
- # \param area_id \type{str} Identifying name of the area to which the component should be reparented
- # \param component \type{QQuickComponent} The component that should be reparented
@pyqtSlot(str, "QVariant")
- def addAdditionalComponent(self, area_id, component):
+ def addAdditionalComponent(self, area_id: str, component):
+ """Add a component to a list of components to be reparented to another area in the GUI.
+
+ The actual reparenting is done by the area itself.
+ :param area_id: dentifying name of the area to which the component should be reparented
+ :param (QQuickComponent) component: The component that should be reparented
+ """
+
if area_id not in self._additional_components:
self._additional_components[area_id] = []
self._additional_components[area_id].append(component)
@@ -1644,10 +1724,13 @@ class CuraApplication(QtApplication):
@pyqtSlot(QUrl, str)
@pyqtSlot(QUrl)
- ## Open a local file
- # \param project_mode How to handle project files. Either None(default): Follow user preference, "open_as_model" or
- # "open_as_project". This parameter is only considered if the file is a project file.
def readLocalFile(self, file: QUrl, project_mode: Optional[str] = None):
+ """Open a local file
+
+ :param project_mode: How to handle project files. Either None(default): Follow user preference, "open_as_model"
+ or "open_as_project". This parameter is only considered if the file is a project file.
+ """
+ Logger.log("i", "Attempting to read file %s", file.toString())
if not file.isValid():
return
@@ -1716,8 +1799,14 @@ class CuraApplication(QtApplication):
if not global_container_stack:
Logger.log("w", "Can't load meshes before a printer is added.")
return
+ if not self._volume:
+ Logger.log("w", "Can't load meshes before the build volume is initialized")
+ return
nodes = job.getResult()
+ if nodes is None:
+ Logger.error("Read mesh job returned None. Mesh loading must have failed.")
+ return
file_name = job.getFileName()
file_name_lower = file_name.lower()
file_extension = file_name_lower.split(".")[-1]
@@ -1731,17 +1820,21 @@ class CuraApplication(QtApplication):
for node_ in DepthFirstIterator(root):
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
fixed_nodes.append(node_)
- 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)
- min_offset = 8
+
default_extruder_position = self.getMachineManager().defaultExtruderPosition
default_extruder_id = self._global_container_stack.extruderList[int(default_extruder_position)].getId()
select_models_on_load = self.getPreferences().getValue("cura/select_models_on_load")
- for original_node in nodes:
+ nodes_to_arrange = [] # type: List[CuraSceneNode]
+
+ fixed_nodes = []
+ for node_ in DepthFirstIterator(self.getController().getScene().getRoot()):
+ # Only count sliceable objects
+ if node_.callDecoration("isSliceable"):
+ fixed_nodes.append(node_)
+ for original_node in nodes:
# Create a CuraSceneNode just if the original node is not that type
if isinstance(original_node, CuraSceneNode):
node = original_node
@@ -1749,8 +1842,8 @@ class CuraApplication(QtApplication):
node = CuraSceneNode()
node.setMeshData(original_node.getMeshData())
- #Setting meshdata does not apply scaling.
- if(original_node.getScale() != Vector(1.0, 1.0, 1.0)):
+ # Setting meshdata does not apply scaling.
+ if original_node.getScale() != Vector(1.0, 1.0, 1.0):
node.scale(original_node.getScale())
node.setSelectable(True)
@@ -1781,19 +1874,15 @@ class CuraApplication(QtApplication):
if file_extension != "3mf":
if node.callDecoration("isSliceable"):
- # Only check position if it's not already blatantly obvious that it won't fit.
- if node.getBoundingBox() is None or self._volume.getBoundingBox() is None or node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
- # Find node location
- offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = min_offset)
+ # Ensure that the bottom of the bounding box is on the build plate
+ if node.getBoundingBox():
+ center_y = node.getWorldPosition().y - node.getBoundingBox().bottom
+ else:
+ center_y = 0
- # If a model is to small then it will not contain any points
- if offset_shape_arr is None and hull_shape_arr is None:
- Message(self._i18n_catalog.i18nc("@info:status", "The selected model was too small to load."),
- title=self._i18n_catalog.i18nc("@info:title", "Warning")).show()
- return
+ node.translate(Vector(0, center_y, 0))
- # Step is for skipping tests to make it a lot faster. it also makes the outcome somewhat rougher
- arranger.findNodePlacement(node, offset_shape_arr, hull_shape_arr, step = 10)
+ nodes_to_arrange.append(node)
# This node is deep copied from some other node which already has a BuildPlateDecorator, but the deepcopy
# of BuildPlateDecorator produces one that's associated with build plate -1. So, here we need to check if
@@ -1812,7 +1901,10 @@ class CuraApplication(QtApplication):
if select_models_on_load:
Selection.add(node)
-
+ try:
+ arrange(nodes_to_arrange, self.getBuildVolume(), fixed_nodes)
+ except:
+ Logger.logException("e", "Failed to arrange the models")
self.fileCompleted.emit(file_name)
def addNonSliceableExtension(self, extension):
@@ -1820,9 +1912,8 @@ class CuraApplication(QtApplication):
@pyqtSlot(str, result=bool)
def checkIsValidProjectFile(self, file_url):
- """
- Checks if the given file URL is a valid project file.
- """
+ """Checks if the given file URL is a valid project file. """
+
file_path = QUrl(file_url).toLocalFile()
workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path)
if workspace_reader is None:
@@ -1840,7 +1931,6 @@ class CuraApplication(QtApplication):
return
selection_pass = cast(SelectionPass, self.getRenderer().getRenderPass("selection"))
if not selection_pass: # If you right-click before the rendering has been initialised there might not be a selection pass yet.
- print("--------------ding! Got the crash.")
return
node = self.getController().getScene().findObject(selection_pass.getIdAtPosition(x, y))
if not node: