diff options
Diffstat (limited to 'cura/CuraApplication.py')
-rwxr-xr-x | cura/CuraApplication.py | 398 |
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: |